Skip to content

Latest commit

 

History

History
414 lines (279 loc) · 24.5 KB

File metadata and controls

414 lines (279 loc) · 24.5 KB

spring-tutorial-23rd

CEOS 백엔드 23기 스프링 튜토리얼

🌱 1주차 미션

1️⃣ spring-tutorial-23rd를 완료해요!

DataGrip에서 데이터추가 후 API 요청 결과를 확인하는 단계에서 서버를 실행했는데도 스키마에 테이블이 생성되지 않아 당황했었다. DataGrip 탐색기를 새로고침하거나 앱을 재실행하였는데도 변함이 없어 원인을 찾던 중 .yml 파일에서 원인을 찾을 수 있었다.

  datasource:
    url: jdbc:mysql://localhost:3306/${database}?allowPublicKeyRetrieval=true&useSSL=false&characterEncoding=UTF-8
    username: root
    password: ${password}
    driver-class-name: com.mysql.cj.jdbc.Driver

	  jpa:
	    hibernate:
	      ddl-auto: create
	    show-sql: true
	    properties:
	      hibernate:
	        format_sql: true

jpa 설정의 들여쓰기를 잘못하여 jpa가 datasource 안에 포함되게 하였다. 이 때문에 스프링이 jpa 설정을 인식하지 못하여 ddl-auto 설정이 동작하지 않았다. 들여쓰기를 수정하여 스프링 설정에서 jpa와 datasource가 서로 동등한 계층에 있도록 했다. 따라서 jpa 설정이 정상 작동하고, 테이블이 자동으로 생성되게 되었다.

이런 들여쓰기 실수는 간단하지만 치명적일 수 있다는 것을 느꼈다. 특히 설정파일에서의 실수는 빌드 시에도 오류가 나타나지 않기 때문에 알아차리기도 쉽지 않다. 따라서 이런 부분을 조금 더 신경을 쓸 수 있도록 해야겠다는 생각을 했다. 또한 오류의 원인을 찾으면서 ddl-auto 설정의 위험성(?)에 대해서도 다시 공부할 수 있어서 좋았다.

ddl-auto:

  • update: 엔티티 변경분만 테이블에 반영 (가장 많이 쓰임)
  • create: 서버 시작 시 기존 테이블을 삭제하고 새로 만듦(데이터가 날아가니 주의, 개발 시에만)
  • none: 자동 생성 기능을 끔

2️⃣ spring이 지원하는 기술들(IoC/DI, AOP, PSA 등)을 자유롭게 조사해요

  1. IoC / DI (제어의 역전 / 의존성 주입)

→ 내가 쓸 부품을 직접 만들지 않고, 외부에서 제공받아 조립하는 것 (부품 = 객체, 외부 = 스프링 컨테이너)

과거에는 개발자가 직접 new 키워드를 사용해 객체를 생성하고 관리했다. 즉, 제어권이 개발자에게 있었는데, 스프링이 이 제어권을 가져가게 된 것이 ‘IoC’이다. 스프링 컨테이너가 프로그램 실행 시점에 필요한 객체들을 미리 만들어두고, 객체가 서로 필요할 때 제공해주는 것이 DI이다.

이를 통해 객체 간의 결합도를 낮춰 객체를 교체하거나 테스트를 하기 훨씬 쉬워졌다.

👇 아래는 ‘땅땅땅’ 프로젝트에서 내가 직접 작성한 코드 중 IoC/DI가 적용된 예시이다.

// com/demoday/ddangddangddang/service/auth/AuthService.java

@Service // 1. 스프링에게 이 클래스를 Bean으로 만들어 달라고 알림 (IoC)
@RequiredArgsConstructor // 2. 롬복을 통한 생성자 자동 생성
public class AuthService {

    // 3. 내가 쓸 객체들을 선언만 해둠 (new 키워드 없음)
    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;
    private final JwtUtil jwtUtil;
    private final EmailService emailService;
    private final RedisExampleService redisService;

    // ... 비즈니스 로직 ...
}

AuthService 는 DB 접근, 암호화, 토큰 등 다양한 객체에 의존하는데, 코드 어디에도 new 키워드는 없다.

@RequiredArgsConstructor 를 통해 final 필드에 대한 생성자가 만들어지고, 스프링은 앱이 켜질 때 이 생성자를 통해 필요한 Bean들을 자동으로 주입(DI)해준다.

  1. AOP (관점 지향 프로그래밍)

→ 핵심 로직 앞뒤로 반복되는 귀찮은 작업들을 비서를 배치하여 처리하는 기술 (비서 = 프록시)

비즈니스 로직을 짜다 보면 트랜잭션 관리, 로깅, 권한 체크, 비동기 처리 등 여러 곳에서 공통으로 필요한 부가 기능이 생긴다. AOP는 이런 부가 기능들을 따로 빼서 모듈화하여 개발자가 원할 때 코드의 앞뒤에 끼우게 해주는 기술이다.

스프링 AOP는 주로 프록시 패턴을 사용한다. 진짜 객체 대신 가짜(프록시) 객체가 요청을 먼저 가로채 부가 작업을 한 뒤 진짜 객체에게 넘겨준다.

👇 마찬가지로 ‘땅땅땅’프로젝트에서의 AOP가 적용된 예시이다.

// com/demoday/ddangddangddang/service/third/JudgmentAsyncExecutor.java

@Component
@RequiredArgsConstructor
@Slf4j
public class JudgmentAsyncExecutor {

    private final ChatGptService chatGptService;
    private final JudgmentRepository judgmentRepository;

    // AOP 적용: 이 메서드를 호출하면 스프링의 프록시가 가로채서
    // 별도의 스레드(threadPoolTaskExecutor)에서 실행하도록 만들어 줌
    @Async("threadPoolTaskExecutor")
    @Transactional // AOP 적용: 프록시가 트랜잭션을 열고, 정상 종료 시 커밋, 에러 시 롤백
    public void executeJudgment(Long caseId, JudgmentStage stage) {
        
        // 1. AI API 호출 (오래 걸리는 메인 비즈니스 로직)
        AiJudgmentDto aiJudgmentDto = chatGptService.judgment(...);
        
        // 2. DB 저장
        judgmentRepository.save(...);
    }
}

복잡한 스레드 생성 코드나, DB 커밋/롤백 코드를 한 줄도 작성하지 않고, @Async@Transactional 어노테이션만을 붙였다. 스프링 AOP가 이 어노테이션을 인식해 알아서 부가기능(비동기 스레드 풀 할당, DB 트랜잭션 경계 설정)을 덧붙여 준다.

동일한 클래스 내에서 @Async 메서드를 호출하면 비동기가 작동하지 않는 Self-Invocation 문제를 겪었다. 이는 스프링 AOP가 프록시 기반으로 동작하기 때문에 외부에서 객체를 호출할 때만 프록시가 개입할 수 있다는 AOP의 핵심 원리를 공부한 경험이 있다.

  1. PSA (일관된 서비스 추상화)

→ 내부 기술이 바뀌어도 내 코드는 수정할 필요 없게 해주는 어댑터.

스프링은 특정 기술에 종속되지 않도록 인터페이스를 제공한다. 개발자는 스프링이 제공하는 추상화된 인터페이스만 보고 개발하면 된다. 나중에 내부 구현 기술이 바뀌어도(Tomcat을 Netty로 바꾸거나, Hibernate 대신 다른 ORM을 쓰는 등) 개발자가 짠 코드는 거의 변경되지 않는다.

👇 ‘땅땅땅’ 프로젝트 속 PSA 예시 ( Spring Data JPA, Spring Web MVC)

// com/demoday/ddangddangddang/repository/CaseRepository.java

// PSA의 결정체: JpaRepository 인터페이스
public interface CaseRepository extends JpaRepository<Case, Long> {
    
    // 개발자는 규칙에 맞게 이름만 작성하면, 
    // 스프링(PSA)이 내부적으로 Hibernate를 조작하는 쿼리와 코드를 다 만들어 줌
    @Query("SELECT c FROM Case c WHERE c.appealDeadline <= :now AND c.status = :status")
    List<Case> findByAppealDeadlineBeforeAndStatus(@Param("now") LocalDateTime now, @Param("status") CaseStatus status);
}

DB와 통신하기 위해 JpaRepository 를 상속받았을 뿐, JDBC Connection을 맺거나 예외 처리를 하는 복잡한 로직을 짜지 않았다. 스프링의 PSA 덕분에 복잡한 DB 접근 기술 대신 표준화 된 방식으로 데이터를 다룰 수 있었다.

또 다른 예로 @RestController@GetMapping 도 PSA다. Servlet 객체를 직접 다루지 않고도 스프링이 추상화해 둔 어노테이션만으로 쉽게 웹 요철을 처리한다.

  1. POJO (Plain Old Java Object)

→ 순수하고 자유로운 평범한 자바 객체

과거 EJB 시절에는 서버의 고급 기능(트랜잭션, 분산 처리 등)을 사용하려면 무조건 프레임워크가 제공하는 무거운 부모 클래스를 상속 받거나 특정 인터페이스를 구현해야만 했다. 즉, 내 코드가 프레임워크에 완전히 종속되어 버려서, 프레임워크를 걷어내면 내 코드를 다 버려야 했고, 무거워서 테스트하기도 너무 힘들었다.

스프링은 여기서 개발자는 POJO 형태의 코드만 만들고, 복잡한 작업은 어노테이션만 붙이면 되도록 하였다. 이로 인해 개발자들은 프레임워크의 규칙에 얽매이지 않고, 순수하게 비즈니스 로직에만 집중할 수 있게 되었다.

👇 ‘땅땅땅’ 프로젝트 속 POJO 예시

// com/demoday/ddangddangddang/dto/ai/AiJudgmentDto.java

@Getter
@NoArgsConstructor
@AllArgsConstructor
public class AiJudgmentDto {

    private String mainArgument;
    private String reasoning;
    private DebateSide winningSide;

}

이 코드를 보면 extends SpringDto 같은 상속이 전혀 없다. 오직 String , DebateSide (직접 만든 Enum)같은 순수 자바 타입으로만 이루어져 있다. 이렇게 순수한 객체임에도 불구하고, 스프링 웹(Web MVC)은 클라이언트에게 응답을 보낼 때 이 순수한 객체를 알아서 JSON 포맷으로 변환해준다.

POJO 상태의 유지를 통해 코드의 가독성을 올리고, 유지보수가 쉬우며, 빠른 테스트가 가능하다.

추가: 스터디에서 Bean 관련 설명 중, Bean 수동 등록 시, @Configuration 클래스 안에서 의도적으로 @Bean 을 생략하고 순수 자바의 new 키워드로 객체를 생성할 때가 있지 않을까란 의문이 생겼다.

실제로도 그런 경우가 존재한다. 모든 객체에 @Bean 을 붙여 스프링 컨테이너에 올리면 싱글톤으로 관리되며 애플리케이션 전역에서 주입받을 수 있게 된다. 하지만 굳이 다른 곳에서는 안 쓰고 오직 해당 클래스 내부에서만 사용된다면, 스프링에게 관리하라고 던져줄 필요 없이 개발자가 직접 new 로 생성해서 쓰고 버리는(가비지 컬렉터에게 맡기기) 것이 메모리나 프레임워크 최적화 관점에서 훨씬 깔끔하다.

👇 ‘땅땅땅’ 프로젝트 내 예시

@Configuration
public class OpenAiConfig {

    // @Bean이 없음 (스프링은 이 객체의 존재를 모름)
    // 오직 아래의 진짜 Bean을 만들기 위한 '내부 조력자(Helper) POJO' 역할만 함
    private OpenAiHeaders createInternalHeaders() {
        OpenAiHeaders headers = new OpenAiHeaders();
        headers.add("Authorization", "Bearer " + secretKey);
        headers.add("Content-Type", "application/json");
        return headers;
    }

    // 이 객체가 진짜 스프링 컨테이너(IoC)에 등록되어 관리되는 객체임
    @Bean 
    public ChatGptClient chatGptClient() {
        // 내가 직접 만든(new) 조력자 객체를 가져와서
        OpenAiHeaders headers = createInternalHeaders(); 
        
        // 진짜 Bean을 조립해서 스프링에게 던져줌 (의존성 주입의 재료로 사용)
        return new ChatGptClient(headers);
    }
}

위 코드에서 createInternalHeaders() 는 앱 전역에서 쓰일 일이 없는 객체이다. 만약 여기에 @Bean 을 붙였다면 불필요하게 스프링 컨테이너의 메모리를 차지하고 다른 곳에서 잘못 주입받을 위험이 생긴다.

3️⃣ Spring Bean 이 무엇이고, Bean 의 라이프사이클과 Bean Scope에 대해 조사해요

Spring Bean이란?

자바에서 new 키워드로 직접 만든 객체는 빈이 아니다. 오직 스프링이 제어권을 가지고 관리하는 객체만이 Spring Bean이다. 즉, 스프링 컨테이너(IoC 컨테이너)가 직접 생성하고, 조립하고, 관리하는 객체이다.

Bean의 라이프사이클

  1. 스프링 컨테이너 생성: 스프링 부트가 실행되며 컨테이너를 만듦.
  2. 스프링 빈 생성: 등록할 빈들의 객체 생성.
  3. 의존성 주입: 객체들이 생성된 후, 서로 필요한 객체들을 연결해 줌.
  4. 초기화 콜백: 의존성 주입이 완료된 후, 빈이 사용되기 전에 딱 한 번 호출되는 마무리 세팅 작업.
  5. 빈 사용: 애플리케이션이 돌아가며 빈들이 열심히 일함.
  6. 소멸 전 콜백: 스프링 컨테이너가 종료되기 직전, 안전하게 자원을 반납하도록 호출함.
  7. 스프링 종료: 빈들이 모두 메모리에서 해제.

Bean Scope

Scope는 빈을 언제까지 살려두고, 몇 개나 만들 것인지 결정한다.

  • Singleton(기본값): 스프링 컨테이너 시작부터 종료까지 딱 1개만 만들어져서 공유된다. 우리가 만든 대부분의 Service, Repository, Controller는 상태를 가지기지 않으므로 싱글톤으로 관리되어 메모리를 아낀다.
  • Prototype: 빈을 요청할 때마다 매번 새로운 객체를 찍어내서 반환한다. 스프링은 생성과 주입까지만 해주고 관리를 포기한다.
  • Web Scopes:
    • request: HTTP 요청이 하나 들어올 때마다 생성되고 응답 시 소멸한다.
    • session: HTTP 세션과 동일한 생명주기를 가진다.

어노테이션이란 무엇이며, Java에서 어떻게 구현될까요?

  • 어노테이션이란?

→ 코드에 붙이는 포스트잇

주석이 사람을 위한 메모라면, 어노테이션은 컴파일러나 스프링 등을 위한 메모다. 그 자체로는 아무런 동작을 하지 않지만, 이 메모를 읽은 스프링 프레임워크가 특정 동작을 수행하게 만든다.

  • 자바에서 어노테이션의 구현

자바 내부적으로 어노테이션은 java.lang.annotation.Annotation 인터페이스를 상속받는 특수한 형태의 인터페이스다. 선언할 때 @interface 키워드를 사용한다.

// 자바 내부의 어노테이션 구현 예시
@Target(ElementType.TYPE) // 어디에 붙일 수 있는가? (클래스, 인터페이스 등)
@Retention(RetentionPolicy.RUNTIME) // 언제까지 살아남는가? (런타임까지)
public @interface Service {
    String value() default "";
}

스프링에서 어노테이션을 통해 Bean을 등록할 때, 어떤 일련의 과정이 일어나는지 탐구해보세요.

클래스에 @Service 와 같은 어노테이션을 하나 붙인다고 Bean이 되는 이유가 뭘까.

  1. 어노테이션 발견: 스프링이 시작되면서 지정된 경로를 스캔하여 @Component (또는 이를 포함한 @Service, @Repository, @Controller 등)가 붙은 클래스를 찾는다.
  2. BeanDefinition 생성: 클래스를 발견하면, 해당 클래스의 정보(이름, Scope, 주입해야 할 의존성, 클래스 경로 등)를 담은 BeanDefinition 이라는 명세서를 만든다.
  3. Bean 생성 및 컨테이너 등록: 스프링의 BeanFactory (IoC 컨테이너)가 이 BeanDefinition 설계도를 바탕으로 실제 자바 객체를 생성하고, 싱글톤 컨테이너 캐시에 저장하여 관리하기 시작한다.

@ComponentScan 과 같은 어노테이션을 사용하여 스프링이 컴포넌트를 어떻게 탐색하고 찾는지의 과정을 깊게 파헤쳐보세요.

스프링 부트를 켜면 무조건 실행되는 메인 클래스 위에는 @SpringBootApplication 이 있다. 이 안을 까보면 @ComponentScan 이 들어있다. 이것의 탐색과정은 매우 정교하다.

  1. 스캔 범위 설정: @ComponentScan 이 붙은 클래스가 위치한 패키지를 Base Package로 설정하고, 그 하위의 모든 패키지를 탐색 대상으로 잡는다.
  2. ASM을 통한 바이트코드 읽기: 스프링은 스캔 단계에서 모든 클래스를 JVM 메모리에 로딩하지 않는다. 대신 ASM이라는 바이트코드 조작 프레임워크를 사용해 .class 파일의 메타데이터만 가볍게 읽어낸다.(성능을 위해)
  3. 필터링 (Include/Exclude): 읽어낸 메타데이터 중, 클래스 레벨에 @Component 메타 어노테이션이 존재하는지 확인한다.
  4. BeanDefinitionRegistry 등록: 조건에 맞는 클래스를 찾으면 BeanDefinition 객체를 만들어 BeanDefinitionRegistry 에 등록한다.

즉, 파일 시스템에서 .class 파일을 뒤져서 어노테이션이 있는지 확인하고, 설계도를 작성해 창고에 넣는 과정이라고 볼 수 있다.

(선택) 하나의 interface를 구현한 service가 여러개 있을때 어떻게 주입 해야할까요?

이 부분은 실무에서 디자인 패턴을 적용할 때 자주 겪는 문제 라고 한다. ‘땅땅땅’ 프로젝트를 예로 들어, 알림을 보내는 NotificationService 인터페이스가 있고, 이를 구현한 SlackNotificationServiceEmailNotificationService가 있다고 가정하자.

@Autowired
private NotificationService notificationService; // 에러 발생 (NoUniqueBeanDefinitionException)

스프링은 둘 중 누굴 주입해야 할 지 몰라 에러가 발생한다. 이를 해결하는 3가지 방법이 있다.

  1. @Qualifier 사용 (명시적 지명)

내가 주입받고 싶은 빈의 이름을 정확히 지명하는 방식이다.

@Autowired
@Qualifier("slackNotificationService") // 클래스명의 맨 앞글자를 소문자로 한 것이 기본 빈 이름
private NotificationService notificationService;
  1. @Primary 사용 (기본값 설정)

여러 구현체 중 하나에게 메인이라고 명시하는 방식이다.

@Service
@Primary // 이걸 붙이면 기본적으로 이 빈이 주입됨
public class SlackNotificationService implements NotificationService { ... }
  1. List 또는 Map으로 모두 다 받기

구현체가 여러 개일 때, 해당 인터페이스를 구현한 모든 빈을 통째로 주입받을 수 있다.

@Service
@RequiredArgsConstructor
public class NoticeFacade {
    
    // Map의 Key는 빈의 이름, Value는 실제 빈 객체가 들어옴
    private final Map<String, NotificationService> notificationServices;

    public void send(String type, String message) {
        // type("slackNotificationService" 등)에 따라 동적으로 서비스 선택
        NotificationService service = notificationServices.get(type);
        service.sendNotification(message);
    }
}

이 방식을 사용하면 나중에 SmsNotificationService 등이 추가되어도 기존 NoticeFacade 코드는 수정할 필요가 없게 된다.

4️⃣ 🔥Spring MVC를 심층 분석해요🔥

  • MVC 패턴과 Spring MVC는 어떻게 다를까요?

MVC 패턴 (Model-View-Controller):

MVC 패턴은 건축 양식이다. 소프트웨어를 개발할 때 화면(View), 비즈니스 데이터 및 로직(Model), 그리고 이 둘을 연결하고 제어하는 역할(Controller)을 분리하자는 범용적인 소프트웨어 디자인 패턴이다.

Spring MVC:

Spring MVC는 MVC 패턴이라는 아이디어를 자바 환경에서 가장 완벽하게 구현해 낸 프레임워크다. 가장 큰 차이점은 Spring MVC가 프론트 컨트롤러 패턴을 도입했다는 것이다. 모든 클라이언트의 요청을 개별 컨트롤러가 직접 받는 것이 아닌, 맨 앞에 있는 DispatcherServlet 이라는 수문장이 모든 요청을 먼저 다 받고, 적절한 컨트롤러로 분배해 주는 구조를 가진다.

  • Servlet은 무엇이고 웹 요청이 어떻게 처리될까요?

Servlet이란?

자바를 사용하여 웹 페이지를 동적으로 생성하는 서버 측 프로그램이다. 클라이언트가 HTTP 요청을 보내면, 개발자가 HTTP 메시지를 파싱하고 응답을 문자열로 일일이 적어주는 것은 너무 힘들다. Servlet은 HTTP 요청을 읽기 쉬운 HttpServletRequest 객체로 만들고, 응답을 쉽게 담을 수 있는 HttpServiceResponse 객체를 제공해 주는 ‘자바의 웹 처리 규약’이다.

웹 요청 처리 과정 (기존 서블릿 기준)

  1. 사용자가 웹 브라우저에서 Post /api/v1/auth/login 등을 요청한다.
  2. 서버가 요청을 받으면, 서블릿을 관리하는 WAS가 새로운 스레드를 하나 만든다.
  3. 해당 요청을 처리할 서블릿 객체의 service() 메서드를 호출한다.
  4. service() 안에서 HTTP Method(GET, POST 등)을 파악하고 doGet(), doPost() 등을 실행한다.
  5. 로직이 끝나면 HTTP 응답을 만들어 브라우저로 보내고, 스레드는 소멸(또는 반납) 된다.
  • 톰캣이 무엇이고 WAS는 무엇일까요?

WAS (Web Application Server):

정적인 HTML 이미지만 보여주는 ‘Web Server(NginX 등)’와 달리, DB를 조회하고 비즈니스 로직을 실행하여 동적인 컨텐츠를 만들어내는 프로그램이다. 내부에 서블릿을 실행하고 관리하는 서블릿 컨테이너를 포함하고 있다.

톰캣 (Tomcat):

이러한 WAS의 한 종류이자, 가장 대표적인 자바 서블릿 컨테이너다. 우리가 Spring Boot 애플리케이션을 실행할 때 따로 톰캣을 설치하지 않아도 서버가 돌아가는 이유는, Spring Boot가 내장 톰캣을 기본으로 포함하고 있기 때문이다.

  • Dispatcher Servlet은 무엇이고 어떻게 동작하는지 직접 흐름을 분석해요.

doDispatch()의 핵심 3단계 흐름

  1. 요청에 매핑되는 핸들러 조회( getHandler )
// 1. 요청에 패핑되는 HandlerExecutionChain 조회
mappedHandler = getHandler(processedRequest);

DispatcherServlet 은 요청받은 URL을 보고 어떤 컨트롤러의 어떤 메서드가 이 일을 처리해야 할 지 찾는다. 이때 컨트롤러 메서드 정보뿐만 아니라, 로직 실행 전후에 거쳐야 할 인터셉터들을 묶어서 HandlerExecutionChain 이라는 객체로 반환한다.

  • 땅땅땅 예시: /api/v1/auth/login 요청 시, AuthControllerlogin() 메서드가 핸들러로 찾아진다.
  1. 핸들러를 실행할 어댑터 조회(getHandlerAdapter )
// 2. 요청을 처리할 HandlerAdapter 조회
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

컨트롤러는 @Controller 로 만들 수도 있고, Controller 인터페이스를 구현해서 만들 수도 있다. DispatcherServlet 는 이 핸들러를 어떻게 실행할지 모르기 때문에, 핸들러의 형태에 맞게 실행시켜 줄 어댑터를 찾는다.

  • 땅땅땅에선 @RequestMapping (또는 @PostMapping) 어노테이션 기반이므로 RequestMappingHandlerAdapter가 찾아진다.
  1. 핸들러 어댑터 실행(ha.handler )
// 3. HandlerAdapter를 통해 컨트롤러 메소드 호출
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

어댑터가 찾아낸 컨트롤러 메서드를 호출한다. 이 과정에서 개발자가 편하게 코딩할 수 있는 두 가지 컴포지트 패턴이 있다.

  1. ArgumentResolver (요청 데이터 변환):
  • 메서드를 호출하기 전 파라미터를 조립한다.
  • ‘땅땅땅’의 로그인 컨트롤러를 보면 login(@RequestBody LoginRequestDto requestDto) 처럼 생겼다.
  • 어댑터는 ArgumentResolver 를 사용해 HTTP Body의 JSON 데이터를 자바 객체(LoginRequestDto)로 변환해서 파라미터로 넣어준다.
  1. ReturnValueHandler (응답 데이터 변환):
  • 컨트롤러가 로그인 성공 후 LoginResponseDto 라는 자바 객체를 반환한다.
  • 어댑터는 ReturnValueHandler (내부의 HttpMessageConverter 가 직접 응답 바디에 데이터를 넣고 null 을 반환한다.)

참고로 과거에는 JSP/HTML 화면을 랜더링하기 위해 ModelAndView 를 반환받아 ViewResolver 를 거쳤지만, @RestController 를 사용하는 REST API 환경에서는 ViewResolver 를 거치지 않고 HttpMessageConverter 가 직접 응답 바디에 데이터를 넣고 null 을 반환한다.

5️⃣ (선택) CGV DB를 모델링해봐요!

  1. 좌석 테이블을 만들지 않기
    • “통로가 없고 빈 곳이 없는 직사각형”, “동일한 타입이면 좌석 형태가 같다”
    • 즉, 일반관(10x10), 특별관(15x15) 처럼 규격이 고정되어 있으므로, 굳이 DB에 모든 좌석 데이터를 100개, 200개씩 미리 만들어둘 필요가 없다.
    • 예매 된 좌석만 DB에 저장하고, 화면에 보여줄 때는 전체 좌석 화면에서 예매 된 좌석만 색깔을 칠하는 방식이 효율적.
  2. 매점 상품과 지점별 재고를 분리 (다대다 관계 해결)
    • 메뉴는 전국 공통이므로 하나만 만든다.
    • 각 지점의 재고는 ‘영화관’과 ‘상품’ 사이의 중간 테이블로 만들어 관리한다.
  3. 중복 예매 방지를 위해 예매된 좌석 상세 테이블 만들기
    • 상영관 ID, 좌석 행, 좌석 열 3개를 묶어서 유니크 키 제약조건 설정하기
  4. 매점 구매 시 환불 X
    • 매점 구매 내역 테이블에서 상태 컬럼 생략 또는 무조건 ‘COMPLETED’

https://www.erdcloud.com/d/PhXPysc9AfrTJbSYq