협력

객체지향 시스템은 자율적인 객체들의 공동체

메시지 전송은 객체 사이의 협력을 위해 사용할 수 있는 유일한 수단

메시지를 수신한 객체는 메서드를 실행해 요청에 응답

외부 객체는 오직 메시지만 전송할 뿐, 메시지가 어떻게 처리될지는 수신한 객체가 직접 결정

>> 객체는 자율적인 존재

 

객체들이 어플리케이션 기능 구현을 위해 수행하는 상호작용을 협력 이라고 함

 

객체를 자율적으로 만드는 가장 기본적인 방법 >> 내부 구현을 캡슐화 하는 것

캡슐화를 통해 변경에 대한 파급효과를 제한

 

상태는 객체가 행동하는데 필요한 정보에 의해 결정

행동은 협력 안에서 객체가 처리할 메시지로 결정

협력은 객체를 설계하는데 필요한 일종의 문맥(Context)을 제공

 

쉽게 말해 

하나의 객체가 혼자 기능을 수행하지 않고 다른 객체에 메시지를 보내 책임을 분산하는 방법

 

public class Order {
    private String orderId;
    private double amount;

    public Order(String orderId, double amount) {
        this.orderId = orderId;
        this.amount = amount;
    }

    public boolean process(PaymentProcessor processor) {
        return processor.pay(orderId, amount);
    }
}

public class PaymentProcessor {
    public boolean pay(String orderId, double amount) {
        System.out.println("결제 요청: 주문번호=" + orderId + ", 금액=" + amount);
        return true;
    }
}

Order 는 스스로 결제를 진행하지 않고, PaymentProcessor 에게 메시지를 보내 결제 기능 위임

자신의 책임 일부를 다른 객체에게 위임하는 것이 협력

 

 

책임

협력에 참여하기 위해 객체가 수행하는 행동

 

하는 것 (Doing)

객체를 생성하거나 계산을 수행하는 등의 스스로 하는 것

public class Calculator {
    // 계산을 스스로 수행하는 책임
    public int add(int a, int b) {
        return a + b;
    }
}

다른 객체의 행동을 시작시키는 것

public class Order {
    private PaymentProcessor paymentProcessor;

    public Order(PaymentProcessor paymentProcessor) {
        this.paymentProcessor = paymentProcessor;
    }

    public void checkout(double amount) {
        // 다른 객체의 행동을 유도 (협력)
        paymentProcessor.processPayment(amount);
    }
}

public class PaymentProcessor {
    public void processPayment(double amount) {
        System.out.println("결제 처리 중: " + amount);
    }
}

다른 객체의 활동을 제어하고 조절하는 것

public class TaskManager {
    private Worker worker;

    public TaskManager(Worker worker) {
        this.worker = worker;
    }

    public void execute() {
        if (!worker.isBusy()) {
            worker.doWork();
        } else {
            System.out.println("작업자가 바쁩니다. 대기 중...");
        }
    }
}

public class Worker {
    private boolean busy;

    public boolean isBusy() {
        return busy;
    }

    public void doWork() {
        busy = true;
        System.out.println("작업 시작");
        // 작업 수행
        busy = false;
    }
}

 

아는 것 (Knowing)

사적인 정보에 관해 아는 것

public class User {
    private String name;
    private String email;

    // 사적인 정보(name, email)를 알고 있음
    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }
}

관련된 객체에 관해 아는 것

public class Order {
    private User customer; // 관련된 객체(User)에 대해 알고 있음

    public Order(User customer) {
        this.customer = customer;
    }

    public String getCustomerEmail() {
        return customer.getEmail(); // 관련 객체의 상태를 조회
    }
}

자신이 유도하거나 계산할 수 있는 것에 관해 아는 것

public class Rectangle {
    private int width;
    private int height;

    public Rectangle(int width, int height) {
        this.width = width;
        this.height = height;
    }

    // 자신의 상태를 기반으로 유도 가능한 정보를 제공
    public int getArea() {
        return width * height;
    }
}

 

 

 

** 책임은 객체지향 설계의 핵심

협력이 중요한 이유도 객체에게 할당할 책임을 결정할 수 있는 문맥을 제공하기 때문

 

Order → PaymentProcessor → PaymentGateway

Order가 결제를 직접 처리하는 게 아니라 PaymentProcessor에 협력 요청

결제 처리는 Order가 아니라 PaymentProcessor의 책임이구나”라고 판단 가능

즉, 협력 구조를 살펴보면 책임 분배 기준을 정할 수 있는 문맥이 되는 것

 

 

!!! 객체지향 설계에서 가장 중요한 것이 책임 !!!

 

 

책임 할당

자율적인 객체를 만드는 가장 기본적인 방법 >> 책임을 수행하는데 필요한 정보를 가장 잘 알고있는 전문가에게 책임을 할당하는것

이를 책임 할당을 위한 Information Expert(정보 전문가) 패턴 이라고 함

 

객체에게 책임 할당을 위해서는 협력이라는 문맥을 정의해야 함

 

public class Screening {
    private Movie movie;
    private LocalDateTime whenScreened;
    private int sequence;

    // 협력 문맥: 예약 요청 → Movie에게 책임 위임
    public Reservation reserve(Customer customer, int audienceCount) {
        Money fee = movie.calculateFee(this, audienceCount); // 협력
        return new Reservation(customer, this, fee, audienceCount);
    }

    // 상태 조회용
    public LocalDateTime getStartTime() {
        return whenScreened;
    }

    public int getSequence() {
        return sequence;
    }
}

책임

  • 예매 요청을 처리하는 책임 (reserve)을 가짐
  • 하지만 요금 계산 책임은 자신이 아님을 인지하고 Movie에게 위임

 협력

  • Movie.calculateFee(this, audienceCount) 호출을 통해 책임을 정보 전문가에게 토스
  • 자신은 Movie, Customer, audienceCount 정보만 알고, 계산 X

 

public class Movie {
    private String title;
    private Duration runningTime;
    private Money fee;
    private DiscountPolicy discountPolicy;

    // 정보 전문가: 요금 계산 책임 보유
    public Money calculateFee(Screening screening, int audienceCount) {
        Money discount = discountPolicy.calculateDiscountAmount(screening); // 협력
        return fee.minus(discount).times(audienceCount);
    }
}

책임

  • 요금 계산 책임 (calculateFee)은 Movie가 가진 고유 정보에 근거하므로 여기서 수행
    • 기본 요금(fee)
    • 할인 정책(discountPolicy)
  • 이는 Information Expert 원칙의 전형적인 사례

협력

  • 할인 계산은 DiscountPolicy 객체에 위임
  • 즉, 할인 세부 전략은 또 다른 전문가(discountPolicy)가 책임지고 수행

 

public class Reservation {
    private Customer customer;
    private Screening screening;
    private Money fee;
    private int audienceCount;

    public Reservation(Customer customer, Screening screening, Money fee, int audienceCount) {
        this.customer = customer;
        this.screening = screening;
        this.fee = fee;
        this.audienceCount = audienceCount;
    }
}

 

책임

  • 예매 결과를 보관하는 단순한 책임만 가짐
  • 특별한 행위(Doing)는 없고, 상태 정보만 저장 → 대표적인 정보 보관 객체

협력 없음

  • 생성된 후 외부에서 참조되는 용도로 사용

 

객체 책임 이유 협력
Movie 요금 계산 (calculateFee) 요금, 할인 정책, 금액 계산 정보를 알고 있음 할인 정책 객체에 위임
Screening 예매 요청 처리 (reserve) 예매 요청을 받고, 어떤 영화인지 알고 있음 Movie에게 요금 계산 요청
Reservation 예매 결과 보관 생성된 예매 정보를 보관 없음 (단순 DTO 성격)

 

 

이 설계가 좋은 이유

  1. Doing 책임Knowing 책임이 명확히 분리됨
  2. 정보를 가장 잘 아는 객체가 그 책임을 가짐 (Information Expert)
  3. 협력(메시지 위임) 을 통해 각 객체는 단일 책임 원칙(SRP) 을 유지함
  4. 변경에 유연 (ex: 할인 정책만 바꿔도 시스템 전체 수정 없이 확장 가능)

 

책임 주도 설계

책임을 찾고, 책임을 수행할 적절한 객체를 찾아 책임을 할당하는 방식으로 협력을 설계하는 방법

 

예매 시스템에서 

  • 시스템이 사용자에게 제공해야 하는 기능 파악
  • 시스템 책임을 더 작은 책임으로 분할
  • 분할된 책임을 수행할 수 있는 적절한 객체나 역할을 찾아 책임 할당
  • 객체가 책임 수행 도중, 다른 객체 도움이 필요한 경우 이를 책임질 적절한 객체, 역할을 탐색
  • 해당 객체, 역할에게 책임을 할당함으로써 두 객체가 협력

 

메시지가 객체를 결정

메시지가 객체를 선택하게 해야 하는 이유

 

1. 객체가 최소한의 인터페이스를 가질 수 있게 됨

필요한 메시지 식별 이전까지 객체 퍼블릭 인터페이스에 어떤 것도 추가하지 않기 때문에

객체는 어플리케이션에 필요한 크기의 퍼블릭 인터페이스를 가질 수 있음

// 메시지를 먼저 식별: "총 주문 금액을 알고 싶다"
order.getTotalAmount(); // 메시지

// 이에 따라 Order 클래스는 이 메서드만 퍼블릭 인터페이스로 갖게 됨
public class Order {
    public Money getTotalAmount() {
        ...
    }
}

 

  • getTotalAmount()만 퍼블릭 API로 노출되므로 불필요한 내부 정보 노출 방지
  • 이렇게 하면 객체의 인터페이스가 작고 깔끔해짐 (정보 은닉 + 유지보수 용이)

 

 

2. 객체는 충분히 추상적인 인터페이스를 가질 수 있게 됨

객체 인터페이스는 무엇을 하는지는 표현, but 어떻게 하는지는 노출 X

메시지는 외부 객체가 요청하는 무언가를 의미하기 때문에 

메시지를 먼저 식별하면 무엇을 수행할 지에 초점 맞추기 가능

 

나쁜 예

// 너무 구체적인 메서드가 퍼블릭으로 노출됨
order.calculateDiscountRate();
order.calculateSubtotal();
order.applyCoupon();
  • 내부 구현이 퍼블릭 API에 노출되어 결합도 높고, 캡슐화 깨짐

 

좋은 예

// "총 결제 금액이 궁금하다"는 메시지
order.getFinalPrice();
  • 내부 로직(할인, 쿠폰, 세금 등)은 숨기고,
  • 단 하나의 의미 있는 메시지로 외부와 소통추상화 유지

 

 

행동이 상태를 결정

객체가 존재하는 이유 >> 협력 참여 위해

객체를 객체답게 만드는 것은 객체의 상태가 아닌 다른 객체에게 제공하는 행동

 

초보자들은 먼저 객체에 필요한 상태가 무엇인지를 결정하고, 그 후에 상태에 필요한 행동을 결정

>> 이런 방식은 객체의 내부 구현이 객체의 퍼블릭 인터페이스에 노출되도록 만들기 때문에 캡슐화를 저해

객체의 내부 구현에 초점을 맞춘 설계 방벙을 데이터-주도 설계(Data-Driven Design) 

라고 함>> 좋지 않음

예시 설명
DTO 객체에 비즈니스 로직 추가 원래 단순 데이터 전송용 객체에 점점 처리 로직을 넣게 됨
User 클래스에 setName(), setEmail()만 존재 정보 보관만 하고 책임 없음 → 아무나 상태 변경 가능
Order 객체가 getItemList().add() 식으로 직접 수정됨 외부에서 내부 리스트 조작 → 캡슐화 위반
DB 테이블 필드 중심으로 POJO 먼저 만든 후 메서드 붙이기 "DB 구조 = 객체 구조"가 되며, 도메인 모델이 아님

 

 

@Entity
public class Order {
    @Id
    private Long id;

    @OneToMany
    private List<OrderItem> items;

    public Money getTotalPrice() {
        return items.stream()
                    .map(OrderItem::getSubtotal)
                    .reduce(Money.ZERO, Money::plus);
    }

    // JPA용 기본 생성자
    protected Order() {}

    public void addItem(OrderItem item) {
        this.items.add(item);
    }
}

이렇게 Entity, domain 을 하나로 섞어쓰지 말고

 

// Entity (Persistence 전용)
@Entity
public class OrderEntity {
    @Id
    private Long id;

    @OneToMany
    private List<OrderItemEntity> items;

    public List<OrderItemEntity> getItems() {
        return items;
    }
}
// Domain (Business Logic 전용)
public class Order {
    private List<OrderItem> items;

    public Money calculateTotalPrice() {
        return items.stream()
                    .map(OrderItem::getSubtotal)
                    .reduce(Money.ZERO, Money::plus);
    }

    public boolean isFreeShipping() {
        return calculateTotalPrice().isGreaterThan(Money.of(100000));
    }
}

DB테이블과 매핑되는 Entity 역할, 비즈니스 로직을 위한 Domain 역할 객체를 나눠 사용

 

 

쉽게 말해서동작을 먼저 생각하고 상태 (필드 등)을 결정해라

 

 

역할

객체지향 설계에서 객체가 특정 문맥(상황)에서 수행해야 할 책임의 집합을 의미
즉, 객체가 어떤 행위자(actor)로서 시스템 안에서 어떤 기능을 담당하는가를 나타냄

 

 

유연하고 재사용 가능한 협력

역할과 책임을 중심으로 객체 간 협력을 설계하여,
구체 클래스가 아닌 추상화(역할)를 기준으로 상호작용하게 하고
그로 인해 객체 간 결합도를 줄이고 교체와 확장이 가능한 구조를 만드는 것

 

// 역할 (추상화된 협력 대상)
public interface DiscountPolicy {
    Money calculateDiscountAmount(Screening screening);
}
// 협력 객체 1: 정액 할인
public class AmountDiscountPolicy implements DiscountPolicy {
    public Money calculateDiscountAmount(Screening screening) {
        return new Money(1000);
    }
}

// 협력 객체 2: 비율 할인
public class PercentDiscountPolicy implements DiscountPolicy {
    public Money calculateDiscountAmount(Screening screening) {
        return screening.getFee().times(0.1);
    }
}
// 협력의 클라이언트: Movie
public class Movie {
    private DiscountPolicy discountPolicy; // 역할에 의존

    public Money calculateFee(Screening screening) {
        Money discount = discountPolicy.calculateDiscountAmount(screening); // 메시지 전송
        return screening.getFee().minus(discount);
    }
}

 

항목 설명
정책 교체 유연 DiscountPolicy 인터페이스만 구현하면 새 정책을 Movie에 손대지 않고 추가 가능
테스트 용이 FakeDiscountPolicy 등을 만들어 단위 테스트 가능
협력 구조 재사용 가능 calculateDiscountAmount() 메시지를 통해 다양한 할인 방식과 유사 구조의 협력 재사용 가능
확장성 확보 TimeDiscountPolicy, HolidayDiscountPolicy 등 무한 확장 가능

 

 

역할 모델링

 

  • 역할 → 책임 → 객체 순으로 설계 흐름을 유지
  • 행위 중심 설계를 유도 (데이터 중심이 아님)
  • 추상적인 역할 단위의 협력 구조를 정의하고,
    후에 구체적인 객체로 매핑하여 유연한 구조를 만들 수 있도록 함
구성요소 설명
역할(Role) 객체가 수행해야 할 기능적 위치/행위자
책임(Responsibility) 역할이 수행해야 할 작업과 정보
협력(Collaboration) 역할들이 어떻게 메시지를 주고받으며 목표를 달성하는지

 

 

 

 

예시 시나리오 : 결제 시스템

고객이 결제를 요청하면, 시스템은 결제 수단을 선택해서 결제를 수행
결제 수단은 카드나 포인트 등으로 다양하게 바뀔 수 있음

 

1. 역할의 정의

역할 책임
결제 요청자 결제 요청 시작 (requestPayment)
결제 처리자 결제 수행 (process())

 

2. 코드로 역할 모델링

public interface PaymentMethod {
    void process(int amount);
}

 

 

3. 실제 협력 객체 정의

public class CardPayment implements PaymentMethod {
    public void process(int amount) {
        System.out.println("카드로 " + amount + "원 결제 완료");
    }
}
public class PointPayment implements PaymentMethod {
    public void process(int amount) {
        System.out.println("포인트로 " + amount + "원 결제 완료");
    }
}

 

4. 역할을 이용하는 클라이언트 객체 

public class Customer {
    private PaymentMethod paymentMethod; // 역할에 의존

    public Customer(PaymentMethod paymentMethod) {
        this.paymentMethod = paymentMethod;
    }

    public void requestPayment(int amount) {
        paymentMethod.process(amount); // 협력 메시지
    }
}

 

 

5. 실행

public class Main {
    public static void main(String[] args) {
        PaymentMethod method = new CardPayment(); // 역할에 대한 구체 객체 주입
        Customer customer = new Customer(method);

        customer.requestPayment(10000); // 카드로 10000원 결제 완료

        // 역할 교체: 포인트로 결제
        customer = new Customer(new PointPayment());
        customer.requestPayment(5000); // 포인트로 5000원 결제 완료
    }
}

 

 

요소 설명
역할 PaymentMethod 인터페이스
책임 process(int amount) 수행
협력 Customer → PaymentMethod 메시지 전달
유연성 카드, 포인트 등 구현 객체 교체 가능

 

 

 

역할과 추상화

역할은 곧 추상화의 이름이며,
추상화는 역할을 코드로 표현하는 수단

 

 

// 역할: PaymentMethod
// 추상화로 표현된 인터페이스
public interface PaymentMethod {
    void process(int amount);
}

 

  • PaymentMethod는 "결제 처리자"라는 역할을 추상화
  • 역할을 수행할 수 있는 객체는 무엇이든 이 인터페이스를 구현하면 됨

실제 객체는 역할을 수행하는 배우(actor)일 뿐

public class CardPayment implements PaymentMethod {
    public void process(int amount) {
        System.out.println("카드로 " + amount + "원 결제");
    }
}

public class PointPayment implements PaymentMethod {
    public void process(int amount) {
        System.out.println("포인트로 " + amount + "원 결제");
    }
}

 

 

  • 이 두 클래스는 같은 역할(PaymentMethod)을 수행하지만, 방식이 다름
  • 즉, 역할은 하나, 구현은 여러 개 → 역할은 추상화고, 객체는 구체화

 

 

 

<<interface>> PaymentMethod      ← 역할 (추상화)
        ▲
        |
+------------------------+
| CardPayment            | ← 역할을 수행하는 구체 클래스
+------------------------+
| PointPayment           | ← 또 다른 구체 클래스

 

  • “역할”은 객체가 시스템 내에서 수행하는 기능적 위치
  • “추상화”는 그 역할을 코드상에서 표현하는 수단 (ex: 인터페이스, 추상 클래스)
  • 객체지향 설계에서는 역할을 기준으로 추상화를 먼저 정의하고 그 역할을 수행하는 여러 객체를 구현하여 협력을 설계하는 것이 핵심

 

'복습 > Object' 카테고리의 다른 글

[Object] 객체지향 프로그래밍  (0) 2025.07.20

 

객체지향

말 그대로 객체를 지향하는 것

진정한 객체지향 패러다임으로 전환은 클래스가 아닌 객체에 초점을 맞출 때 얻을 수 있다

 

이를 위해서는

1. 어떤 클래스가 필요한지 이전에 어떤 객체가 필요한지 고민 

2. 객체를 독립적인 존재가 아니라 기능 구현을 위해 협력하는 공동체의 일원으로 인식

 

도메인

소프트웨어는 사용자가 어떤 문제를 해결하기 위해 만들어진다

영화 예매 시스템의 목적은 영화를 좀 더 쉽고 빠르게 예매하려는 사용자의 문제를 해결하는 것

이처럼 사용자가 프로그램을 사용하는 분야를 도메인 이라고 함

 

 

 

일반적으로 클래스의 이름은 대응되는 도메인 개념의 이름과 동일/유사하게 지어야 함

클래스 사이의 관계도 최대한 도메인 개념 사이에 맺어진 관계와 유사하게 

 

이 원칙에 따라

영화 > Movie

상영 > Screening

할인정책 > DiscountPolicy

비율 할인 정책 > PercentDiscountPolicy

금액 할인 정책 > AmountDiscountPolicy

할인 조건 > DiscountCondition

순번 조건 > SequenceCondition

기간 조건 > PeriodCondition

예매 > Reservation

 

클래스로 구현하기

도메인 개념을 반영하는 클래스 생성 후, 적절한 프로그래밍 언어를 이용해 이 구조를 구현

public class Screening {
    private Movie movie;
	private int sequence;
    private LocalDateTime whenScreened;
    
    public Screening(Movie movie, int sequence, LocalDateTIme whenScreened) {

		this.movie = movie;
        this.sequence = sequence;
        this.whenScreened = whenScreened;
    }
    
    public LocalDateTime getStartTime(){
    	return whenScreened;
    }
    
    public boolean isSequence(int sequence){
    	return this.sequence = sequence;
    }
    
    public Money getMovieFee(){   
    	return movie.getFee();
    }

}

 

여기서 인스턴스 변수는 private, 메서드는 public

..클래스의 경계를 구분 짓기 위해 ..

 

외부에서는 객체의 속성에 직접 접근할 수 없고, 적절한 public 메서드를 통해 내부상태 변경 가능하게 해야 함

why?

경계의 명확성이 객체의 자율성을 보장하기 때문

더 나아가 프로그래머에게 구현의 자유를 제공하기 때문

 

 

자율적인 객체

1. 객체는 상태와 행동을 함께 가지는 복합적인 존재

2. 객체가 스스로 판단하고 행동하는 자율적인 존재

두 가지 사실은 깊은 연관이 있음

 

보통의 경우

객체를 상태, 행동을 함꼐 포함하는 식별 가능한 단위로 정의

객체라는 단위 안에 데이터와 기능을 한덩어리로 묶음으로써 문제영역의 아이디어를 적절하게 표현 가능

이처럼 데이터와 기능을 객체 내부로 함께 묶는 것을 캡슐화

 

대부분 객체지향 언어는 외부에서 접근을 통제할 수 있는 접근제어 메커니즘 함께 제공

public, private, protected 등

 

 

캡슐화와 접근제어는 객체를 두 부분으로 분할

외부에서 접근 가능한 부분으로 퍼블릭 인터페이스(Pulbic Interface)

오직 내부에서 접근 가능한 부분으로 구현(Implemetation)

 

!! 인터페이스와 구현의 분리 원칙은 객체지향 프로그래밍에 있어서 매우 중요 !!

 

 

프로그래머의 자유

프로그래머의 역할을 클래스 작성자(Class creator), 클라이언트 프로그래머(Client programmer)로 구분하는것이 객체지향 개발에 유용

 

클래스 작성자는 새로운 데이터 타입을 프로그램에 추가

클라이언트 프로그래머는 클래스 작성자가 추가한 데이터 타입을 사용

 

클라이언트 프로그래머의 목표는 클래스를 엮어서 어플리케이션을 빠르고 안정적으로 구축하는 것

클래스 작성자는 클라이언트 프로그래머에게 필요한 부분만 공개하고 나머지는 숨겨서 클라이언트 프로그래머에 대한 영향을 고려하지 않고 내부 구현 가능

이를 구현 은닉(Implementation hiding)

 

변경될 가능성이 있는 세부적인 구현을 private영역 안에 감춰 변경으로 인한 혼란을 최소화 할 수 있음

 

협력하는 객체들의 공동체

public class Screening {
    private Movie movie;

    public Reservation reserve(Customer customer, int audienceCount) {
        return new Reservation(customer, this, calculateFee(audienceCount),
                audienceCount);
    }

    private Money calculateFee(int audienceCount) {
        return movie.calculateMovieFee(this).times(audienceCount);
    }
}

 

public class Money {
    public static final Money ZERO = Money.wons(0);

    private final BigDecimal amount;

    public static Money wons(long amount) {
        return new Money(BigDecimal.valueOf(amount));
    }

    public static Money wons(double amount) {
        return new Money(BigDecimal.valueOf(amount));
    }

    Money(BigDecimal amount) {
        this.amount = amount;
    }

    public Money plus(Money amount) {
        return new Money(this.amount.add(amount.amount));
    }

    public Money minus(Money amount) {
        return new Money(this.amount.subtract(amount.amount));
    }

    public Money times(double percent) {
        return new Money(this.amount.multiply(BigDecimal.valueOf(percent)));
    }

    public boolean isLessThan(Money other) {
        return amount.compareTo(other.amount) < 0;
    }

    public boolean isGreaterThanOrEqual(Money other) {
        return amount.compareTo(other.amount) >= 0;
    }
}

 

public class Reservation {
    private Customer customer;
    private Screening Screening;
    private Money fee;
    private int audienceCount;

    public Reservation(Customer customer, Screening Screening, Money fee, int audienceCount) {
        this.customer = customer;
        this.Screening = Screening;
        this.fee = fee;
        this.audienceCount = audienceCount;
    }
}

 

영화를 예매하기 위해 Screening, Movie, Reservation 인스턴스들은 서로의 메서드를 호출하며 상호작용

이처럼 시스템의 어떤 기능 구현을 위해 객체 사이에 이뤄지는 상호작용을 협력(Collaboration) 이라고 함

 

 

 

협력에 관한 짧은 이야기

객체는 다른 객체의 인터페이스에 공개된 행동을 수행하도록 요청(Request) 가능

요청받은 객체는 자율적인 방법에 따라 요청 처리 후 응답(Response)

 

객체가 다른 객체와 상호작용할 수 있는 유일한 방법은 메시지를 전송하는 것 뿐

다른 객체에게 요청이 온걸 메시지 수신 이라고 함

 

메시지, 메서드를 구분짓는것은 매우 중요 !!

다형성(Polymorphism)의 개념

 

 

할인요금 구하기

public abstract class DiscountPolicy {
    private List<DiscountCondition> conditions = new ArrayList<>();
    
    public DiscountPolicy(DiscountCondition ... conditions) {
        this.conditions = conditions;
    }
    
    public Money calculateDiscountAmount(Screening screening) {
        for(DiscountCondition each : conditions) {
            if (each.isSatisFiedBy(screening)) {
                return getDiscountAmount(screening)
            }
        }
        
        return Money.ZERO;
    }
    
    abstract Money getDiscountAmount(Screening screening);
}

 

 

DiscountPolicy 는 할인 여부와 요금 계산에 필요한 전체 흐름은 정의하지만, 실제로 요금을 계산하는 부분은 추상메서드 

getDiscountAmount에게 위임

실제로는 상속받은 클래스가 오버라이딩한 메서드가 실행

이처럼 부모 클래스에 기본적인 알고리즘을 구현하고

중간에 필요한 처리를 자식 클래스에게 위임하는 디자인 패턴을

TEMPLATE METHOD 패턴 이라고 부름

 

 

상속성과 다형성

컴파일 시간 의존성과 실행 시간 의존성

 

어떤 클래스가 다른 클래스에 접근할 수 있는 경로르 가지거나, 해당 클래스의 객체 메서드를 호출할 경우

두 클래스 사이에 의존성이 존재한다고 함

 

만약 영화 요금 계산을 위해 할인 정책을 적용하고 싶다면 

Movie 인스턴스 생성 시, 인자로 AmountDiscountPolicy 인스턴스를 전달하면 됨

Movie avartar = new Movie("아바타",
	Duration.ofMinutes(120),
    Money.wons(10000),
    new AmountDiscountPolicy(Money.wons(800), ...));

이제 실행 시, Movie 인스턴스는 AmountDiscountPolicy 클래스 인스턴스에 의존하게 됨

 

비율 할인 정책을 적용하고 싶다면 대신 PercentDiscountPolicy 인스턴스 전달

Movie avartar = new Movie("아바타",
	Duration.ofMinutes(120),
    Money.wons(10000),
    new PercentDisCountPolicy(0,1,....));

 

 

 

차이에 의한 프로그래밍

상속은 객체지향에서 코드 재사용을 위해 가장 널리 사용되는 방법

상속만으로 기존 클래스가 가지고 있는 모든 속성, 행동을 새로운 클래스에 포함 가능

 

이처럼 부모클래스와 다른 부분만 추가해서 새로운 클래스를 쉽고 빠르게 만드는 방법을

차이에 의한 프로그래밍(Programming by difference) 라고 함

 

상속과 인터페이스

상속을 통해 자식 클래스는 인터페이스에 부모 클래스의 인터페이스를 포함하게 되고 

부모 클래스가 수신할 수 있는 모든 메시지를 수신 가능

외부 객체는 자식 클래스를 부모 클래스와 동일한 타입으로 간주 가능

 

이처럼 자식 클래스가 부모 클래스를 대신하는 것을 업캐스팅(Upcasting) 이라고 함

 

다형성

위 코드에서 Movie는 동일한 메시지를 전송하지만, 실제로 어떤 메서드가 실행될 지는 메시지를 수신하는 객체 클래스가 무엇인가에 따라 달라짐

이를 다형성 이라고 함

 

동일한 메시지를 수신했을 때, 객체 타입에 따라 다르게 응답할 수 있는 능력

즉, 다형적인 협력에 참여하는 객체들은 모두 같은 메시지를 이해할 수 있어야 함

 

메시지에 응답을 위해 실행될 메서드를 컴파일 시점이 아닌 실행 시점에 결정

메시지와 메서드를 실행 시점에 바인딩 >> 지연 바인딩 (lazy binding)/동적 바인딩(dynamic binding)

전통적인 함수 호출처럼 컴파일 시점에 실행될 함수나 프로시저를 결정하는 것 >> 초기 바인딩(early binding)/정적 바인딩(static binding)

 

** 상속

구현 상속 (implementation inheritance) - 서브 클래싱

인터페이스 상속 (interface inheritance) - 서브 타이핑

상속은 구현 상속이 아닌, 인터페이스 상속을 위해 사용

인터페이스 재사용이 아닌, 구현 재사용을 목적으로 한다면 변경에 취약한 코드를 짜게 된다

>> 클래스 상속 말고 인터페이스 상속 써라

 

 

 

인터페이스와 다형성

인터페이스?

UI : user interface 할때 그 인터페이스

상호작용, 접점 등의 의미

 

이게뭔데

 

구현에 대한 고리 없이 다형적인 협력에 참여하는 클래스들이 공유 가능한 요소

특정 기능 구현에 대한 약속 이라고 보면 됨

 

인터페이스는 객체지향 프로그래밍의 특징에 대해 설명 가능

 

예를 들어 

public interface Food {
	public String taste();
}

 

Food 라는 인터페이스가 있고 맛을 리턴하는 taste 라는 메서드가 미구현상태로 선언 되어 있을 때 

 

public void Pizza implements Food {

	public String taste(){
    
    	.. 맛을 표현하는 로직 ..
    }
}

과 같이 맛을 표현하는 메서드를 구현해서 사용 가능

 

피자 말고 치킨이라는 클래스를 만들고 싶다면?

public void Chicken implements Food {

	public String taste(){
    
    	.. 맛을 표현하는 로직 ..
    }
}

동일하게 사용 가능

 

Pizza 와 Chicken 클래스의 taste는 다르기에 이를 표현하는 로직은 다르겠지만,

이 클래스들은 음식의 맛을 표현하는 기능을 약속하는 Food 라는 인터페이스를 상속받았기에 

내가 Pizza, Chicken, ...... 기타 음식들 클래스의 실제 로직을 알지 못해도 

taste() 메서드를 통해 그 음식의 맛 정보를 가져올 수 있음

>> 이게 바로 추상화

[무슨 동작을 하는지] 에 집중하고, 어떻게 구성되어있는지는 몰라도 됨

 

또한

Food pizza = new Pizza();
Food chicken = new Chicken();
...

이처럼 Food 라는 하나의 타입으로 다양한 구현체를 만들고 다룰 수 있다

>> 이게 바로 다형성

 

더 자세한 전략 패턴에 대해 설명하자면

interface PaymentStrategy {
    void pay(int amount);
}

class CardPayment implements PaymentStrategy {
    public void pay(int amount) {
        System.out.println("카드로 " + amount + "원 결제");
        .. 
        카드 실적에 따른 혜택
        마일리지 적립
        ..
    }
}

class KakaoPay implements PaymentStrategy {
    public void pay(int amount) {
        System.out.println("카카오페이로 " + amount + "원 결제");
        ..
        카카오페이 할인
        멤버십 적립
        수수료 할인 
        ...
    }
}

PaymentStrategy 인터페이스를 상속받는 CardPayment와 KakaoPay 클래스가 있을 때

 

class OrderService {
    private PaymentStrategy strategy;

    public OrderService(PaymentStrategy strategy) {
        this.strategy = strategy;
    }

    public void checkout(int amount) {
        strategy.pay(amount);  // 다형성 적용
    }
}

결제 기능이 필요한 OrderService 클래스에서 생성자에 PaymentStrategy 객체를 전달받고(의존성 주입)

checkout 메서드를 통해 결제 전략의 pay라는 메서드를 실행하게 됨

>> 여기서 다형성 발생

실제 checkout의 동작 로직은 OrderService를 생성할 때 어떤 전략을 주입했느냐에 따라 달라짐

 

카드결제와 카카오페이 결제의 내부 로직에서 어떤 동작을 하는지 상세하게 몰라도 pay라는 기능을 다른 곳에서 사용 가능

>> 이게 추상화

 

쉽죠?

 

 

인터페이스와 트레이드오프

트레이드오프 >> 단점, 비용

인터페이스를 쓰는것이 무조건 좋은건 아님

 

class SimpleCalculator {
    public int add(int a, int b) {
        return a + b;
    }
}

간단한 덧셈 결과를 위한 클래스

 

interface Calculator {
    int add(int a, int b);
}

class SimpleCalculator implements Calculator {
    public int add(int a, int b) {
        return a + b;
    }
}

인터페이스를 사용하게 되면 오히려 코드량이 증가

 

 

코드 재사용

상속은 코드를 재사용 하기 위해 널리 사용되는 방법

합성(composition) 이라는 방법이 더 좋다?

:: 다른 객체의 인스턴스를 자신의 인스턴스 변수로 포함해서 재사용하는 방법

 

 

상속

상속은 설계에 안좋은 영향을 미치기도 함

 

첫째, 캡슐화 위반

상속을 사용하기 위해서는 부모 클래스의 내부 구조를 알아야됨 >> 여기부터 캡슐화 위반

자식 클래스가 부모 클래스에 강하게 결합되도록 만들기 때문 >> 부모가 바뀌면 자식도 함께 변경될 확률 높아짐

 

둘째, 설계 유연성 저하

자식, 부모 관계를 컴파일 시점에 결정

따라서 런타임에 객체 종류를 변경하는 것이 불가능

 

예를 들어

실행 시점에 금액할인 정책인 영화를 비율 할인 정책으로 변경하고 싶은 경우

아래와 같이

class Movie {
    private String title;

    public Movie(String title) {
        this.title = title;
    }

    public int calculatePrice(int basePrice) {
        return basePrice;  // 기본 가격 반환
    }

    public String getTitle() {
        return title;
    }
}

class AmountDiscountMovie extends Movie {
    private int discountAmount;

    public AmountDiscountMovie(String title, int discountAmount) {
        super(title);
        this.discountAmount = discountAmount;
    }

    @Override
    public int calculatePrice(int basePrice) {
        return basePrice - discountAmount;
    }
}

class PercentDiscountMovie extends Movie {
    private double percent;

    public PercentDiscountMovie(String title, double percent) {
        super(title);
        this.percent = percent;
    }

    @Override
    public int calculatePrice(int basePrice) {
        return (int)(basePrice * (1 - percent));
    }
}
public class App {
    public static void main(String[] args) {
        // 처음엔 금액 할인 정책
        AmountDiscountMovie movie = new AmountDiscountMovie("인셉션", 1000);
        int price = movie.calculatePrice(12000);
        System.out.println("금액 할인 가격: " + price);  // 11000

        // 실행 중 할인 정책을 비율 할인으로 바꾸고 싶음
        // 그러나 movie = new PercentDiscountMovie(...) 불가능 (타입 불일치)

        // 유일한 방법: 새 객체 생성 + 상태 복사
        PercentDiscountMovie newMovie = new PercentDiscountMovie(movie.getTitle(), 0.1);
        int newPrice = newMovie.calculatePrice(12000);
        System.out.println("비율 할인 가격: " + newPrice);  // 10800
    }
}

상속을 사용한 설계에서는 AmountDiscountMovie의 인스턴스를 PercentDiscountMovie의 인스턴스로 변경해야 함

이미 생성된 객체 클래스 변경은 불가능하기 때문에 변경을 위해선

PercentDiscountMovie 인스턴스 생성 후 AmountDiscountMovie의 상태를 복사하는 방법 뿐

>> 부모, 자식의 관계가 강한 결합을 가지기 때문

 

public class Movie {

	private DiscountPolicy discountPolicy;
    
    public void changeDiscountPolicy(DiscountPolicy discountPolicy){
    	this.discountPolicy = discountPolicy;
    }
}

 

Movie avatar = new Movie("아바타",
		Duration.foMinutes(120),
        Money.wons(10000),
        new AmountDiscountPolicy(Money.wons(800), ...));
        
avatar.changeDiscountPolicy(new PrcentDiscountPolicy(0, 1, ...));

금액 할인 정책이 적용된 영화에 비율 할인 정책이 적용되도록 변경하는 것은 새로운 DiscountPolicy 인스턴스를 연결하는 것으로 간단하게 해결됨

 

 

또 다른 예

 

상속

class Engine {
    void start() {
        System.out.println("엔진 시동");
    }
}

class Car extends Engine {
    void drive() {
        start(); // 직접 사용 가능
        System.out.println("운전 중...");
    }
}

 

  • Car는 Engine의 하위 클래스 → is-a 관계
  • 하지만 상속 구조는 강하게 결합되어 변경에 취약

합성

class Engine {
    void start() {
        System.out.println("엔진 시동");
    }
}

class Car {
    private Engine engine;

    public Car(Engine engine) {
        this.engine = engine;
    }

    void drive() {
        engine.start(); // 위임
        System.out.println("운전 중...");
    }
}

 

  • Car는 Engine을 필드로 가지고 있음(has-a)
  • engine.start()는 위임(delegation)
  • 나중에 ElectricEngine, DieselEngine으로도 바꾸기 쉬움 → 유연성↑

 

 

 

합성

Movie는 요금계산을 위해 DiscountPolicy 코드를 재사용

>> Movie 가 DiscountPolicy 인터페이스를 통해 약하게 결합됨

 

이처럼 인터페이스에 정의된 메시지를 통해서만 코드를 재사용하는 방법을 합성 이라고 함

 

인터페이스에 정의된 메시지를 통해서만 재사용 가능 >> 효과적인 캡슐화

의존하는 인스턴스 교체 쉬움 >> 설계 유연

 

코드를 재사용하는 경우에는 상속보다 합성을 선호

다형성을 위해 인터페이스를 재사용하는 경우, 상속과 합성 함께 사용

'복습 > Object' 카테고리의 다른 글

[Object] 역할, 책임, 협력  (2) 2025.07.21

https://school.programmers.co.kr/learn/courses/30/lessons/150368?language=java

 

프로그래머스

SW개발자를 위한 평가, 교육의 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프

programmers.co.kr

입력

2차원 배열 users는 각 유저의 할인 기준과 구매 기준 금액
각 요소는 [할인 기준 %, 구매 기준 금액] 형식
정수 배열 emoticons는 이모티콘 정가 리스트이며, 각 이모티콘마다 10%, 20%, 30%, 40% 중 하나의 할인율을 선택 가능

 

핵심 아이디어

각 이모티콘에 대해 10%, 20%, 30%, 40% 할인율을 적용한 모든 조합을 완전탐색
각 조합마다 모든 유저가 어떻게 반응하는지를 시뮬레이션
사용자는 자신이 설정한 할인 기준 이상인 이모티콘만 구매 대상으로
총 구매 금액이 기준 금액 이상이면 이모티콘 플러스에 가입하고, 구매 X
그 외의 경우에는 구매한 이모티콘들의 금액을 모두 합산
가입자 수를 우선으로 비교하고, 같을 경우 총 판매 금액이 높은 조합을 선택

 

변수 설명

plus: 현재까지 나온 최대 플러스 가입자 수
buy: plus 기준에서의 최대 이모티콘 판매 금액
arr: 현재 할인율 조합을 저장하는 배열 (이모티콘 개수와 동일한 길이)
recursive: 가능한 모든 할인율 조합을 탐색하는 재귀 함수
calc: 현재 할인 조합에서 유저 반응을 계산하는 함수
userPer: 해당 유저가 구매를 고려하는 최소 할인율
userLimit: 해당 유저의 플러스 가입 기준 금액
sum: 유저가 실제 구매한 총 금액
cnt: 현재 조합에서 플러스 가입한 유저 수
buySum: 실제 구매가 발생한 총 금액

 

예시

- 입력
users = { {40, 10000}, {25, 10000} }
emoticons = {7000, 9000}

가능한 할인 조합은 4^2 = 16가지이다
예를 들어 할인율 조합이 [40, 40]이면
7000 * 0.6 = 4200
9000 * 0.6 = 5400
유저 1은 40% 이상만 보기 때문에 두 이모티콘 모두 구매
총합 9600 → 10000 미만 → 구매함
유저 2는 25% 이상 조건 만족, 동일하게 총합 9600 → 구매함
따라서 플러스 가입자 0명, 구매 총액 19200원
이와 같은 방식으로 모든 조합을 비교해 가장 많은 가입자 수와 가장 높은 판매금액 조합을 선택

- 출력

[최대 플러스 가입자 수, 해당 조건에서의 최대 판매 금액] 형태의 정수 배열을 반환

 

 

정답코드

import java.util.*;

class Solution {
    private int plus = 0;
    private int buy = 0;

    public int[] solution(int[][] users, int[] emoticons) {


        int[] arr = new int[emoticons.length];

        recursive(arr, 0, users, emoticons);




        return new int[]{plus, buy};
    }


    public void recursive(int[] arr, int startIdx, int[][] users, int[] emoticons){

        if(startIdx == arr.length){

            calc(arr, users, emoticons);
            return;
        }
        
        for(int i=10; i<=40; i+=10){

            arr[startIdx] = i;
            recursive(arr, startIdx+1 ,users, emoticons);
        }
    }

    private void calc(int[] arr, int[][] users, int[] emoticons){

        int cnt = 0;
        int buySum = 0;

        for(int i=0; i<users.length; i++){

            int userPer = users[i][0];
            int userLimit = users[i][1];
            int sum = 0;

            for(int j=0; j<arr.length; j++){

                if(arr[j] >= userPer){

                    sum += emoticons[j] * (100 - arr[j]) / 100;
                }
            }

            if(sum >= userLimit){

                // 유저 구매 상한보다 높으면 이모티콘 플러스 가입
                cnt++;
            } else{
                // 낮으면 그냥 구매
                buySum+=sum;
            }
        }
            if(cnt > plus){
                plus = cnt;
                buy = buySum;
            }
            if(cnt == plus){

                if(buy < buySum){
                    buy = buySum;
                }
            }
    }
}

@DateTimeFormat 

 

Spring에서 문자열 형태의 날짜/시간을 LocalDate, LocalDateTime, Date 등으로 변환할 때 사용하는 어노테이션

주로 Controller parameter, @ModelAttribute, Dto 등에 사용

 

 

 

GET /members?from=2025-07-01T00:00:00&to=2025-07-09T23:59:59

 

@GetMapping("/members")
public List<Member> getMembersByDateRange(
        @RequestParam("from")
        @DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") LocalDateTime from,

        @RequestParam("to")
        @DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") LocalDateTime to) {

    return memberService.getMembersBetween(from, to);
}

 

 

 

 

 

** 타입 ISO 기본 포맷 예시

LocalDate yyyy-MM-dd 2025-07-09
LocalTime HH:mm:ss 14:30:00
LocalDateTime yyyy-MM-dd'T'HH:mm:ss 2025-07-09T14:30:00

 

 

 

Dto에서의 활용

public class SearchRequest {

    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate startDate;

    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate endDate;

    // getters/setters
}

 

 

변환 에러처리

@RestControllerAdvice
public class GlobalExceptionHandler {

    // 날짜 파라미터 형식이 잘못된 경우 처리
    @ExceptionHandler({ MethodArgumentTypeMismatchException.class, BindException.class })
    public ResponseEntity<ErrorResponse> handleDateTimeFormatException(Exception ex) {
        return ResponseEntity
                .badRequest()
                .body(new ErrorResponse("INVALID_DATE_FORMAT", "날짜 형식이 잘못되었습니다. 예: yyyy-MM-dd 또는 yyyy-MM-dd'T'HH:mm:ss"));
    }

    // 기타 예외 처리
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGenericException(Exception ex) {
        return ResponseEntity
                .status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(new ErrorResponse("INTERNAL_ERROR", "서버 오류가 발생했습니다."));
    }
}

 

 

예외 클래스 발생 상황

MethodArgumentTypeMismatchException @RequestParam 변환 실패 (LocalDate, LocalDateTime 등)
BindException @ModelAttribute 또는 폼 바인딩 실패
HttpMessageNotReadableException @RequestBody 변환 실패 (JSON 날짜 오류 등)

 

 

 

 

@PathVariable을 이용한 경로 변수 처리

 

spring MVC에서 URL 경로에 포함된 값을 메서드의 파라미터로 바인딩할 때 사용하는 어노테이션

REST API 설계에서 자주 사용되며, 리소스의 식별자(ID, 이름 등) 를 전달할 때 적합

 

예시

@GetMapping("/members/{id}")
public Member getMemberById(@PathVariable("id") Long id) {
    return memberService.findById(id);
}

 

요청 예시

GET localhost:8080/members/123

 

위와 같이 members/ 뒤의 숫자를 통해 가변경로 처리 가능

 

 

 

 

Controller Exception 처리

Spring MVC에서 Exception을 처리하는 방식에는 크게 세 가지 방식 존재

범위 방식 특징 권장 여부
전역 처리 @RestControllerAdvice / @ControllerAdvice 모든 컨트롤러에서 공통 처리 표준방식
전역 저수준 HandlerExceptionResolver DispatcherServlet 수준 예외 가로채기 특수목적용
필터/서블릿 Filter, HttpServlet Spring 컨텍스트 외부에서 처리 보안/로깅 용도

 

 

 

@ExceptionHandler 

@RestController
@RequestMapping("/members")
public class MemberController {

    @GetMapping("/{id}")
    public Member get(@PathVariable Long id) {
        return memberService.findById(id); // 예외 가능성
    }

    @ExceptionHandler(MemberNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(MemberNotFoundException ex) {
        return ResponseEntity.status(HttpStatus.NOT_FOUND)
                .body(new ErrorResponse("NOT_FOUND", ex.getMessage()));
    }
}

 

개별 컨트롤러에 에러 처리 메서드 선언

 

 

@RestControllerAdvice, @ControllerAdvice

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MemberNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleMemberNotFound(MemberNotFoundException ex) {
        return ResponseEntity.status(HttpStatus.NOT_FOUND)
                .body(new ErrorResponse("MEMBER_NOT_FOUND", ex.getMessage()));
    }

    @ExceptionHandler(MethodArgumentTypeMismatchException.class)
    public ResponseEntity<ErrorResponse> handleTypeMismatch(MethodArgumentTypeMismatchException ex) {
        return ResponseEntity.badRequest()
                .body(new ErrorResponse("INVALID_PARAMETER", "요청 파라미터 형식이 잘못되었습니다."));
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGeneric(Exception ex) {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(new ErrorResponse("INTERNAL_ERROR", "서버 내부 오류"));
    }
}

 

위처럼 @RestControllerAdvice 어노테이션을 사용한 클래스에서 

ExcptionHandler 메서드를 모아 전역으로 처리 가능

 

 

** RestControllerAdvice, ControllerAdvice 차이

 

RestController와 Controller의 차이와 같음

Response 의 형식에 대한 차이

 

 

항목 @ControllerAdvice @RestControllerAdvice
응답 처리 방식 View 이름 또는 Model 반환 JSON, XML 등 객체 직렬화 반환
@ResponseBody 포함 여부 ❌ 포함 안 됨 (수동 설정 필요) ✅ 자동 포함 (@ResponseBody 내장)
주 사용 대상 JSP, Thymeleaf 기반의 웹 MVC REST API, JSON 기반 서비스
반환 타입 ModelAndView, String, Model 등 POJO 객체 (ErrorResponse, Map 등)
도입 버전 Spring 3.2 이상 Spring 4.3 이상 (Spring Boot 1.4+)

 

 

쉽게 말해서 View를 반환하냐, json 응답 반환하냐 차이

대부분의 경우 이 방식 사용 권장

 

 

 

HandlerExceptionResolver

 

Spring MVC에서 제공하는 저수준 예외 처리 인터페이스

@ControllerAdvice나 @ExceptionHandler보다 먼저 실행되는 전역 예외 처리 수단

 

 

언제사용?

로깅 시스템 또는 알림 시스템과 연계 모든 예외를 공통 처리하거나 Slack/Email 전송
Spring이 처리하지 못하는 예외에 대한 대응 DispatcherServlet 수준에서 직접 처리
View 기반 프로젝트에서 특정 예외 → 특정 페이지 리턴 예: 404.html, 500.jsp 등 매핑 처리
Exception 흐름 완전 차단 (기존 핸들러 무시) 필터처럼 동작시킴

 

 

예시

@Component
public class CustomExceptionResolver implements HandlerExceptionResolver {

    @Override
    public ModelAndView resolveException(HttpServletRequest request,
                                         HttpServletResponse response,
                                         Object handler,
                                         Exception ex) {

        // 예외 로깅
        System.err.println("예외 발생: " + ex.getClass().getSimpleName());

        // 응답 상태 코드 설정
        response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);

        // REST API의 경우 직접 JSON 응답 작성 (비권장 방식)
        // 또는 아래처럼 View를 지정
        ModelAndView mav = new ModelAndView("error/globalError");
        mav.addObject("errorMessage", ex.getMessage());

        return mav;
    }
}

 

+ Java config 등록

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
        resolvers.add(new CustomExceptionResolver());
    }
}

 

 

비교

항목 HandlerExceptionResolver  @RestControllerAdvice
동작 시점 DispatcherServlet 수준 HandlerAdapter 이후
예외 흐름 차단 가능 (더 이상 예외 던지지 않음) 예외는 전달되며 순서 중요
REST 응답 지원 비직관적 (직접 응답 작성 필요) 자동 JSON 직렬화
사용 목적 로깅, 비표준 처리 일반적인 예외 처리

 

 

Filter, HttpServlet 방식

Spring MVC 바깥의 계층에서 예외를 잡는 저수준 방식

 

목적  설명
Spring MVC 진입 전 예외 처리 예: 인증 필터, 로깅 필터, WAS 레벨 예외
Spring이 잡지 못하는 500 에러 대응 DispatcherServlet 밖에서 발생하는 예외 감지
커스텀 응답(JSON) 강제 처리 스프링이 처리하지 않는 응답 직접 구성
보안 필터 체인 / API Gateway 대응 인증, 권한 필터 등과 연계

 

 

예시

@Component
public class ExceptionHandlingFilter implements Filter {

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {

        HttpServletResponse response = (HttpServletResponse) res;

        try {
            chain.doFilter(req, res);
        } catch (Exception ex) {
            // 예외 로깅
            ex.printStackTrace();

            // 커스텀 JSON 응답
            response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            response.setContentType("application/json");
            response.getWriter().write("{\"error\": \"" + ex.getMessage() + "\"}");
        }
    }
}

 

  • DispatcherServlet 이전 단계에서 예외 처리 가능
  • JSON 응답 수동 처리 필요
  • SecurityFilterChain 앞뒤 어디든 배치 가능

 

 

 

 

Filter, SecurityFilter

클라이언트 요청
   ↓
[서블릿 필터 (Filter)]
   ↓
[Spring Security FilterChainProxy]
   ↓
[DispatcherServlet (Spring MVC)]
   ↓
@Controller 등

 

 

  • 일반 Filter는 Spring Security보다 먼저 실행됨
  • Spring Security는 자체 필터 체인을 갖고 있음 (FilterChainProxy로 래핑됨)

 

 

 

 

교재 내용은 매우 레거시한 내용

 

이 챕터에서 살펴봐야 할 내용인 

HttpSession

HandlerInterceptor

쿠키

 

이 세가지에 대해 간략하게 설명

 

 

 

컨트롤러에서 HttpSession 사용하기

 

public class test (HttpSession session){

	...
}

public class test (HttpServletRequest request) {
	HttpSession session = reqeust.getSession();
    ..
}

 

파라메터로 HttpSession을 받는 방식, HttpServletRequest.getSession() 으로 세션 가져오는 방식

두 개 모두 사용 가능

 

HttpSession 객체에서는 Session Attribute 접근 가능

 

session.getAttribute("authInfo", authInfo);

와 같이 사용

 

 

인터셉터 사용하기

 

HandlerInterceptor 인터페이스를 상속받는 Interceptor 커스텀 클래스를 구현해서

인터셉터 사용 가능

 

preHandle :: 컨트롤러 객체 실행 이전

postHandle :: 컨트롤러 객체 실행 이후, 뷰 실행 전

afterCompletion :: 뷰 실행 이후

 

각각의 시점에 맞게 동작에 관해 인터셉터 구현 가능

 

 

 

 

** 이거 구현하는거보다 

Aop

Spring security FilterChain

Servlet Filter 

사용

 

대부분의 인터셉터의 경우 인증 상태 검사, 권한 검사, 로깅, XSS 대응 등에 사용하는데

그냥 Security Filter 에서 구현하는게 나음

 

 

컨트롤러에서 쿠키 사용

 

@CookieValue

 

쿠키 생성

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;

@Controller
@RequestMapping("/hello")
public class HelloController {
    @PostMapping
    public String submit(HttpServletResponse response) {

      //쿠키  객체 생성, "cookieName"이라는 이름으로 쿠키를 생성하고, 그 값은 "cookieValue"로 설정
      Cookie rememberCookie = new Cookie("cookieName", "cookieVlaue");

      // 쿠키 경로 설정, "/"는 모든 경로에서 사용하겠다는 뜻
      rememberCookie.setPath("/");

      // 쿠키를 유지할 시간 설정(단위 : 초)
      rememberCookie.setMaxAge(60*60*24*30); // 30일 동안 쿠키 유지.

      response.addCookie(rememberCookie);

      return "hello";

    }

}

 

 

쿠키 전달

import org.springframework.web.bind.annotation.CookieValue;
import javax.servlet.http.Cookie;

@Controller
@RequestMapping("/hello")
public class HelloController {
    @GetMapping
    public String form(@CookieValue(value = "cookieName", required = false) Cookie coookie) {

        //이름이 "cookieName"인 쿠기가 존재한다면?
        if(cookie != null) {

              //cookieValue 변수에 쿠키 값을 저장한다.
             String cookieValue = cookie.getValue();         
        }

        return "hello";
    }
}

매핑 어노테이션을 이용한 경로 매핑

 

@GetMapping

@PostMapping

@RequestMapping 

 

등을 사용하여 url 매핑

 

 

굳이 

GetMapping, PostMapping 쓰는 이유?

 

가독성, 명확성

하나의 url 에 대해 GET, POST 모두 매핑 가능

 

@GetMapping("/page")
public String getPage(){

	...
}

@PostMapping("/page")
public String editPage(){

	...
}

이처럼 동일한 url에 대해 GET, POST 각각 나눠서 매핑이 가능

 

 

 

요청 파라미터 접근

 

 

HttpServletRequest

@PostMapping("/test/step1"){
public String handleStep1(HttpServletRequest request){
	String agreeParam = request.getParam("agree");
    	 ...
 }

 

@RequestParam

@PostMapping("/test/step1"){
public String handleStep1(@RequestParam(value = "agree", defaultValue = 
"false")Boolean agreeVal){
	if(!agreeVal){
    return "test/step1";
    }
    return "test/step2";
 }

 

 

리다이렉트 처리

 

@GetMapping("test/step1")
public String handleStep2Get(){
	return "redirect:/test/step2";
}

 

 

커맨드 객체를 이용해서 요청 파라미터 사용하기

 

 

HttpServletRequest 방식으로 여러 파라미터를 받으면

 

@PostMapping("/test/step3")
public String handleStep3(HttpServletRequest request) {
	String email = request.getParameter("email");
    String name = request.getParameter("name");
	String password = request.getParameter("password");
	String confirmPassword = request.getParameter("confirmPassword");
    
    RegisterRequest regReq = new RegisterRequest();
    regReq.setEmail(email);
    regReq.setName(name);
    ...
}

와 같이 파라미터 하나하나 다 HttpServletRequest 에서 추출해야 함

 

but

커맨드 객체를 사용하면

 

 

public class RegisterRequest(){

	private String name;
    private String email;
    ...
    
    
    public void setName(name){
    	name = this.name;
    }
    
    public String getName(){
    
   		return name;
    }
    
    ...
}
@PostMapping("/test/step3")
public String handleStep3(RegisterRequest reqReq){
	...
}

 

와 같이 객체 안의 필드와 파라미터를 매핑해줌

 

그냥 우리가 흔히 쓰는 dto 라고 보면 됨

 

 

뷰 JSP 코드에서 커맨드 객체 사용하기

<p><strong>${registerRequest.name}님</strong></p>

와 같이 jsp 에서 커맨드 객체 사용 가능

 

 

@ModelAttribute 어노테이션으로 커맨드 객체 속성 이름 변경

커맨드 객체에 접근할 때 속성 이름을 변경하고 싶으면 

public String handleStep3(@ModelAttribute("formData") RegisterRequest regReq){

	...
}

와 같이 사용

 

주요 에러 발생 상황

요청 매핑 애노테이션과 관련된 주요 익셉션

  • 404 에러: 요청 경로를 처리할 컨트롤러가 존재하지 않거나, WebMvcConfigurer를 이용한 설정이 없거나 뷰 이름에 해당하는 JSP 파일이 존재하지 않는다면 발생하는 에러.
  • 405 에러: 지원하지 않는 전송 방식을 사용한 경우 발생하는 에러.
    ex) POST 방식만 처리하는 요청 경로를 GET방식으로 연결하면 발생

@RequestParam이나 커맨드 객체와 관련된 주요 익셉션

  • 400에러:
    -요청 파라미터의 값을 @RequestParam이 적용된 파라미터 타입으로 변환할 수 없는 경우 발생
    -요청 파라미터 값을 커맨드 객체에 복사하는 과정에서도 발생. 만약 커맨드 객체의 프로퍼티가 int 타입인데 요청 파라미터의 값이 "abc"라면, "abc"를 int 타입으로 변환할 수 없기에 에러 발생

Model을 통해 컨트롤러에서 뷰에 데이터 전달하기

컨트롤러는 뷰가 응답 화면을 구성하는데 필요한 데이터를 생성해서 전달해야하는데 이때 사용하는 것이 Model이다.
뷰에 데이터를 전달하는 컨트롤러는 다음 두가지를 하면 된다.

  • 요청 매핑 애노테이션이 적용된 메서드의 파라미터로 Model을 추가
  • Model 파라미터의 addAttribute()메서드로 뷰에서 사용할 데이터 전달
@Controller
public class HaewonController{
	@RequestMapping("/test")
    public String test(Model model, @RequestParam(value = "name",
    required = false)String name){
		model.addAttribute("greeting","안녕하세요"+name);
        return "test";
     }
}

addAttribute()메서드의 첫번째 파라미터는 속성 이름이다. 뷰 코드는 이 이름을 사용해서 데이터에 접근한다.
위 코드에서는 greeting을 사용해 데이터에 접근하고 있다.
JSP에선 ${greeting}을 사용해서 속성값에 접근한다.

지금까지 구현한 컨트롤러는 두가지 특징이 있었다.

  • Model을 이용해서 뷰에 전달할 데이터 설정
  • 결과를 보여줄 뷰 이름을 리턴

지금까진 이 둘을 따로 처리한 예시들을 봤으나 ModelAndView는 모델과 뷰 이름을 함께 제공하기에 이를 사용하면 이 두 가지를 한번에 처리할 수 있다.

 

 

커맨드객체 : 중첩, 콜렉션 프로퍼티

 

jsp 에서 체크박스처럼 같은 속성이름으로 여러 데이터가 들어올 때

커맨드 객체에 

	private List<String> checkBoxValues;

와 같이 선언해주면 리스트에 해당 데이터가 들어옴

이거뭐 다아는거라 설명할 필요성이 없네

 

 

Model을 통해 컨트롤러에서 뷰에 데이터 전달하기

 

model.addAttribute("data", data);

처럼 사용하면

jsp에서 

${data}

로 사용 가능

 

 

https://school.programmers.co.kr/learn/courses/30/lessons/42860

 

프로그래머스

SW개발자를 위한 평가, 교육의 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프

programmers.co.kr

 

입력

문자열 name은 조이스틱으로 만들어야 하는 목표 이름을 의미
모든 문자는 A~Z로 구성되며, 시작 상태는 모두 'A'로 초기화됨

 

핵심 아이디어

세로 방향은 각 문자마다 A에서 목표 문자까지의 최소 조작 수를 계산
가로 방향은 커서를 어떻게 움직이면 최소인지 탐색하며 연속된 A 구간을 활용해 최적 경로 계산
가장 먼저 연속된 A 구간의 위치를 파악하고, 앞뒤로 돌아가는 여러 경로 중 이동 횟수가 가장 작은 경우를 선택
총 조작 횟수는 세로 조작 수의 합과 가로 조작 수를 더한 값

 

변수설명

alphabet은 알파벳 대문자 A~Z를 담은 문자열
answer는 최종 조작 횟수를 누적하는 변수
horizontal은 최소 좌우 이동 횟수를 계산하는 메서드
vertical은 문자 하나에 대한 최소 상하 이동 횟수를 계산하는 메서드
minMove는 가능한 커서 이동 경로 중 최소 이동 거리
moveCase1은 오른쪽 이동 후 뒤로 돌아가는 방식의 이동 거리
moveCase2는 끝까지 갔다가 왼쪽으로 되돌아오는 방식의 이동 거리
to는 현재 문자가 알파벳에서 몇 번째에 위치하는지 계산한 값

 

예시

name이 "JAZ"인 경우
J는 A에서 9번 이동
A는 그대로
Z는 A에서 1번 역방향 이동
가로 이동은 J → A → Z가 최적인 경우이므로
세로 조작 합은 9 + 0 + 1 = 10
가로 조작은 1
총 조작 횟수는 11

 

정답코드

package week_5;

public class week5_joystick {

    String alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";

    public int solution(String name) {
        int answer = 0;

        int horizontal = horizontal(name);
        for (char c : name.toCharArray()) {

            int vertical = vertical(c);

            System.out.println("A to " + c + ": " + vertical);
            answer += vertical;
        }
        return answer + horizontal;
    }

    private int horizontal(String name) {
        int len = name.length();
        int minMove = len - 1; // 초기값: 오른쪽으로 쭉 이동

        for (int i = 0; i < len; i++) {
            int next = i + 1;

            // 연속된 'A'가 있는 부분 skip
            while (next < len && name.charAt(next) == 'A') {
                next++;
            }

            // 처음부터 i까지 가고 → 뒤로 돌아가서 끝까지
            int moveCase1 = i * 2 + (len - next);
            // 끝까지 갔다가 < 돌아와서 i까지
            int moveCase2 = (len - next) * 2 + i;

            minMove = Math.min(minMove, Math.min(moveCase1, moveCase2));
        }

        return minMove;
    }

    private int vertical(char c) {

        // 여기에 위, 아래로 돌리는거 중 뭐가 더 빠른가 판단
        // 시작은 무조건 A
        int from = 0;
        int to = alphabet.indexOf(c);
        return Math.min(Math.abs(to - from), 26 - Math.abs(to - from));
    }
}

https://school.programmers.co.kr/learn/courses/30/lessons/388353

 

프로그래머스

SW개발자를 위한 평가, 교육의 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프

programmers.co.kr

 

입력

storage는 창고 상태를 나타내는 문자열 배열로 각 문자는 하나의 화물
requests는 작업 요청 목록으로 한 글자 요청은 지게차, 두 글자 이상은 크레인 작업을 의미

 

핵심 아이디어

storage를 2차원 배열로 변환하고 테두리를 만들어 외곽 판별을 쉽게 함
크레인 요청은 해당 알파벳을 전체 탐색하며 즉시 제거
지게차 요청은 BFS를 통해 바깥에서 접근 가능한 동일 알파벳 화물만 제거
모든 요청 수행 후 남아있는 화물의 개수를 반환

 

변수설명

input은 외곽 테두리를 포함한 전체 창고 상태를 담는 2차원 문자열 배열
border는 접근 가능 여부를 판단하는 마크 배열로 1은 접근 가능
dx, dy는 상하좌우 방향 이동을 위한 배열
q는 BFS 탐색에 사용되는 큐
visited는 BFS 중 방문 여부를 체크하는 배열
current는 현재 탐색 중인 좌표
target은 제거할 화물의 알파벳
count는 제거되지 않고 남아 있는 화물 수

 

예시

storage가 ["AB", "CD"], requests가 ["A", "B", "CD"]인 경우
"A"는 지게차 요청이므로 바깥에서 접근 가능한 "A"만 제거
"B"도 지게차 요청으로 같은 방식 수행
"CD"는 크레인 요청으로 "C", "D"를 전체 탐색하여 전부 제거
결과적으로 남은 화물의 수를 반환

 

정답코드

package week_5;

import java.util.Arrays;
import java.util.LinkedList;
import java.util.Queue;

public class week5_crain {


    int[][] border;


    public int solution(String[] storage, String[] requests) {
        int answer = 0;


        String[][] input = new String[storage.length+2][storage[0].length()+2];
        border = new int[input.length][input[0].length];
        for (int i = 0; i < input.length; i++) {
            for (int j = 0; j < input[0].length; j++) {
                if (i == 0 || i == input.length - 1 || j == 0 || j == input[0].length - 1) {
                    border[i][j] = 1;
                }
            }
        }
        // 주어진 화물 정보 이차원 배열로
        // 테두리는 null
        for(int i=0; i<=storage.length-1; i++){
            String origin = storage[i];
            for(int j=0; j<= storage[i].length()-1; j++){
                input[i+1][j+1] = String.valueOf(origin.charAt(j));
            }
        }

        for (String s : requests){
            if(s.length()>=2)
                crain(input, s.substring(0,1));
            else
                bfs(input, s);

        }


        return count(input);
    }

    // 지게차
    private void bfs(String[][] input, String target) {
        int[] dx = {0, 0, 1, -1};
        int[] dy = {1, -1, 0, 0};

        Queue<int[]> q = new LinkedList<>();
        int[][] visited = new int[input.length][input[0].length];

        q.add(new int[]{0, 0});
        visited[0][0] = 1;

        while (!q.isEmpty()) {
            int[] current = q.poll();
            int cx = current[0];
            int cy = current[1];

            for (int i = 0; i < 4; i++) {
                int nx = cx + dx[i];
                int ny = cy + dy[i];

                if (nx < 0 || ny < 0 || nx >= input.length || ny >= input[0].length) continue;
                if (visited[nx][ny] == 1) continue;

                visited[nx][ny] = 1;

                if (input[nx][ny] != null && input[nx][ny].equals(target)) {
                    border[nx][ny] = 1; // 근처에 있어서 접근 가능, 제거 대상
                } else if (input[nx][ny] == null) {
                    q.add(new int[]{nx, ny});
                }
            }
        }

        // 제거 수행
        for (int i = 0; i < input.length; i++) {
            for (int j = 0; j < input[0].length; j++) {
                if (border[i][j] == 1 && input[i][j] != null && input[i][j].equals(target)) {
                    input[i][j] = null;
                }
            }
        }
    }

    // 크레인 사용
    private void crain(String[][] input, String target){

        for(int i=0; i<input.length; i++){
            for(int j=0; j<input[0].length; j++){
                if(input[i][j] !=null && input[i][j].equals(target)){
                    input[i][j]=null;
                    border[i][j]=1;
                }
            }
        }
    }


    // 정답 count
    private int count(String[][] input) {
        int count = 0;
        for (int i = 0; i < input.length; i++) {
            for (int j = 0; j < input[0].length; j++) {
                if (input[i][j] != null) {
                    count++;
                }
            }
        }
        return count;
    }

}

https://school.programmers.co.kr/learn/courses/30/lessons/86971

 

프로그래머스

SW개발자를 위한 평가, 교육의 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프

programmers.co.kr

 

입력

정수 n은 송전탑의 개수
2차원 정수 배열 wires는 각 송전탑을 연결하는 전선 정보

 

핵심 아이디어

모든 간선 중 하나씩 제거해보며 네트워크를 두 개로 분리
각 경우마다 DFS를 통해 한 쪽 네트워크의 노드 수를 구하고
전체 노드 수와 비교해 두 네트워크의 크기 차이를 계산
모든 경우를 탐색하면서 차이의 최솟값을 정답으로 선택

 

변수설명

list는 송전탑 간 연결 정보를 저장하는 인접 리스트
visited는 DFS 방문 여부를 저장하는 배열
cnt는 하나의 네트워크에 속한 송전탑 수
diff는 두 네트워크의 송전탑 개수 차이
dfs 함수는 한 쪽 네트워크에 포함된 송전탑의 개수를 구하기 위해 사용

 

예시

n이 9이고 wires가 [[1,3],[2,3],[3,4],[4,5],[4,6],[4,7],[7,8],[7,9]]인 경우
각 간선을 하나씩 제거해 네트워크를 두 개로 나누고
DFS로 한 쪽 크기를 구해 크기 차이 계산
가장 차이가 적은 경우를 찾아 반환
결과는 1

 

정답코드

    public int solution(int n, int[][] wires) {
        int answer = -1;

        // 여기서 노드 간선 하나씩 끊기
        for(int i=0; i<wires.length; i++){

            // 그래프 초기화
            List<List<Integer>> list = new ArrayList<>();
            for (int j = 0; j <= n; j++) {
                list.add(new ArrayList<>());
            }

            // 간선 연결
            for(int j=0; j<wires.length; j++){

                if(i == j) continue;
                int a = wires[j][0];
                int b = wires[j][1];

                list.get(a).add(b);
                list.get(b).add(a);
            }

            int[] visited = new int[n+1];
            Arrays.fill(visited, 0);

            int cnt = dfs(1, list, visited);
            int diff = Math.abs(n - 2 * cnt);
            if (answer == -1 || diff < answer) {
                answer = diff;
            }
        }

        return answer;
    }


    // dfs 는 단순히 연결되어있는 노드 개수 구하는데 사용
    public int dfs(int node, List<List<Integer>> list, int[] visited){

        visited[node] = 1;

        int cnt = 1;

        // 현재 노드와 연결된 간선 탐색
        for(int i : list.get(node)){

            if(visited[i] != 1){
                cnt += dfs(i, list, visited);
            }
        }

        return cnt;
    }

 

https://school.programmers.co.kr/learn/courses/30/lessons/159993

 

프로그래머스

SW개발자를 위한 평가, 교육의 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프

programmers.co.kr

입력

문자열 배열 maps는 미로의 정보를 담고 있으며 각 문자는 한 칸을 의미
'S'는 시작점, 'L'은 레버, 'E'는 도착점, 'O'는 빈칸, 'X'는 벽을 의미

 

핵심 아이디어

미로에서 S → L → E 순서로 이동할 수 있는 최단 경로를 각각의 BFS로 계산
시작점에서 레버까지, 레버에서 도착점까지 두 번의 경로 탐색 수행
각 경로가 존재하지 않으면 -1을 반환
두 경로가 모두 존재하는 경우 각각의 거리 합에 시작점과 레버, 레버와 도착점 간 이동을 포함해 총 거리 계산

 

변수설명

dx, dy는 4방향 이동을 위한 좌표 변화량
map은 문자열 배열 maps를 2차원 문자열 배열로 변환한 맵 정보
start, lever, end는 각각 S, L, E의 좌표를 담은 정수 배열
q는 BFS에 사용되는 탐색 큐
distance는 방문 여부 및 이동 거리를 기록하는 2차원 정수 배열
curX, curY는 현재 탐색 중인 좌표
newX, newY는 이동 후 좌표

 

예시

maps가 ["SOO", "OXL", "OEO"]인 경우
S에서 L까지의 최단 거리 2
L에서 E까지의 최단 거리 2
총 경로는 존재하며 이동 횟수는 2 + 2 + 2 = 6
결과는 6

 

정답코드

import java.util.*;

class Solution {
    static int[] dx = {0, 0 ,1, -1};
    static int[] dy = {1, -1, 0, 0};

    public int solution(String[] maps) {
        int answer = 0;

        String[][] map = new String[maps.length][maps[0].length()];

        int[] start = new int[2];
        int[] lever = new int[2];
        int[] end = new int[2];

        // String[] 배열을 map으로
        for(int i=0; i<maps.length;i++){
            for (int j = 0; j < maps[i].length(); j++) {
                char ch = maps[i].charAt(j);
                if(ch == 'S'){
                    map[i][j] = "S";
                    start[0] = i;
                    start[1] = j;
                } else if(ch == 'L'){
                    map[i][j] = "L";
                    lever[0] = i;
                    lever[1] = j;
                } else if(ch == 'E'){
                    map[i][j] = "E";
                    end[0] = i;
                    end[1] = j;
                }
                else if(ch == 'X')
                    map[i][j] = "X";
                else if(ch == 'O')
                    map[i][j] = "O";
            }
        }

        int a = bfs(map, start, lever);
        int b = bfs(map, lever, end);

        if(a != -1 && b != -1)
            return a+b+2;
        else return -1;

    }


    public int bfs(String[][] map, int[] start, int[] end){

        int n = map.length;
        int m = map[0].length;

        // bfs 사용을 위한 큐
        Queue<int[]> q = new LinkedList<>();

        // 방문 노드 체크
        int[][] distance = new int[map.length][map[0].length];
        for(int[] v : distance)
            Arrays.fill(v, -1);

        int curX = start[0];
        int curY = start[1];

        // q에 시작값 삽입
        q.add(new int[]{curX, curY});

        // 반복
        while(!q.isEmpty()){
            System.out.println(q.size());
            int[] now = q.poll();
            curX = now[0];
            curY = now[1];

            // 목적지 도착
            if (curX == end[0] && curY == end[1]) {
                return distance[curX][curY];
            }

            // 4방향 탐색
            for(int i=0; i<4; i++){
                int newX = curX+dx[i];
                int newY = curY+dy[i];

                // 미로 범위 바깥일 경우, 이미 방문한경우, 벽인경우 >> 무시
                if (newX < 0 || newX >= n || newY < 0 || newY >= m || distance[newX][newY] > 0 || map[newX][newY].equals("X")) {
                    continue;
                }

                // 방문횟수 + 1
                distance[newX][newY] = distance[curX][curY] + 1;
                q.add(new int[]{newX, newY});
            }
        }

        // 목적지까지 경로 없는경우 -1 return
        return -1;
    }

}

+ Recent posts