Web/spring

[Spring] 기초

개발자 쿠키 2022. 8. 22. 15:41

IoC (Inversion of Control, 제어의 역전)

객체의 생성, 연결, 소멸에 대한 제어권이 개발자에서 스프링으로 넘어간 것입니다. 개발자는 무엇을 쓸지만 선언하고, 언제 만들어서 어떻게 넣어줄지는 스프링이 결정해요.

꼬리 질문 : 제어권이 넘어가면 뭐가 좋나요?

사용하는 쪽이 구현체를 직접 생성하지 않으므로 결합도가 낮아집니다. 구현체를 교체해도 사용하는 코드를 수정할 필요가 없어요.

DI (Dependency Injection, 의존성 주입)

객체가 필요한 의존 객체를 직접 new 하지 않고, 외부에서 주입받는 방식입니다. IoC라는 개념을 구현하는 대표적인 수단이에요.

꼬리 질문 : IoC와 DI의 차이는 뭔가요?

IoC는 제어권이 프레임워크로 넘어갔다는 넓은 개념이고, DI는 그 개념을 실현하는 구체적인 기법입니다. IoC를 구현하는 방법에는 DI 말고 서비스 로케이터 패턴 같은 것도 있습니다.

스프링 빈과 컨테이너

스프링 빈(Bean) : 스프링 컨테이너가 생성하고 관리하는 객체. 개발자가 new로 직접 만든 객체는 빈이 아님

스프링 컨테이너(IoC 컨테이너) : 빈을 담아두고 생명주기를 관리하는 공간

최상위 인터페이스는 BeanFactory이고, 실무에서 쓰는 것은 이를 확장한 ApplicationContext입니다. ApplicationContext에는 국제화, 이벤트 발행, 리소스 로딩 같은 부가 기능이 추가되어 있어요.

POJO와 PSA

POJO (Plain Old Java Object) : 특정 프레임워크의 클래스를 상속하거나 인터페이스를 구현하지 않는 순수한 자바 객체. 스프링은 비즈니스 코드를 POJO로 유지한 채 부가 기능을 붙이는 것을 지향

PSA (Portable Service Abstraction) : 기술이 바뀌어도 코드를 바꾸지 않도록 일관된 추상화 계층을 제공하는 것. JDBC를 쓰든 JPA를 쓰든 @Transactional 하나로 동일하게 동작

의존성 주입 3가지 방식

방식 특징
생성자 주입 권장 방식. 생성자가 하나면 @Autowired 생략 가능
수정자(Setter) 주입 선택적이거나 변경 가능성이 있는 의존관계에 사용
필드 주입 필드에 @Autowired. 간편하지만 권장하지 않음
@Service                                          // 컴포넌트 스캔 대상으로 등록해 스프링이 빈으로 관리하게 함
@RequiredArgsConstructor                          // final 필드만 모아 생성자를 자동 생성. 주입 코드를 직접 안 써도 됨
public class OrderService {

    private final OrderRepository orderRepository; // final로 선언해 주입 이후 변경 불가. 불변성 보장
    private final PaymentClient paymentClient;     // 의존성이 늘면 생성자 파라미터도 늘어 책임 과다를 눈으로 확인 가능

    public Long order(Long itemId) {               // 테스트에서는 new OrderService(mockRepo, mockClient)로 주입 가능
        return orderRepository.save(itemId);       // 스프링 컨테이너 없이 순수 자바로 테스트할 수 있는 구조
    }
}

꼬리 질문 : 왜 생성자 주입을 권장하나요?

1. final 선언이 가능해서 불변성을 보장합니다.

2. 의존성이 누락되면 애플리케이션 구동 시점에 바로 실패하므로 문제를 일찍 발견합니다.

3. 스프링 없이 new로 객체를 만들어 테스트할 수 있습니다.

4. 순환 참조가 있으면 구동 시점에 잡아냅니다. 필드 주입은 런타임에 터집니다.

빈 등록과 조회

@Component와 컴포넌트 스캔 : @ComponentScan이 지정된 패키지 하위를 훑어서 @Component가 붙은 클래스를 자동으로 빈 등록

@Controller, @Service, @Repository : 모두 @Component를 포함한 특수화 애노테이션. @Repository는 데이터 접근 계층의 예외를 스프링의 DataAccessException으로 변환

@Configuration과 @Bean : 외부 라이브러리 객체처럼 소스를 수정할 수 없는 클래스를 빈으로 등록할 때 사용

꼬리 질문 : @Configuration을 붙이면 뭐가 달라지나요?

스프링이 해당 클래스를 CGLIB 프록시로 감싸서, @Bean 메서드를 여러 번 호출해도 항상 같은 인스턴스를 반환하도록 보장합니다. 붙이지 않으면 호출할 때마다 새 객체가 생성되어 싱글톤이 깨집니다.

@Autowired는 기본적으로 타입으로 조회합니다. 같은 타입의 빈이 둘 이상이면 NoUniqueBeanDefinitionException이 발생하고, @Qualifier로 이름을 지정하거나 @Primary로 우선순위를 주어 해결합니다.

빈 스코프와 생명주기

기본값은 싱글톤 스코프입니다. 컨테이너당 인스턴스를 하나만 만들어 공유해요. 요청이 올 때마다 객체를 생성하지 않으므로 메모리와 생성 비용을 아낄 수 있습니다. 그 외에 prototype(조회할 때마다 새 인스턴스 생성 후 관리 종료), request, session 같은 웹 스코프가 있습니다.

꼬리 질문 : 싱글톤 패턴과 스프링 싱글톤의 차이는 뭔가요?

싱글톤 패턴은 private 생성자와 static 메서드로 직접 구현해야 해서 코드가 지저분해지고, 구체 클래스에 의존하게 되며, 테스트에서 교체가 어렵습니다. 스프링은 컨테이너가 인스턴스를 관리하므로 클래스는 평범한 POJO로 두면서 싱글톤을 보장합니다.

꼬리 질문 : 싱글톤이라서 주의할 점은요?

모든 요청이 같은 인스턴스를 공유하므로 빈에 상태를 두면 안 됩니다. 특정 클라이언트에 의존적인 필드가 있으면 동시성 문제가 발생해요. 무상태(stateless)로 설계하고, 값은 지역 변수나 파라미터로 처리합니다.

빈 생명주기

객체 생성 → 의존관계 주입 → 초기화 콜백 → 사용 → 소멸 전 콜백 → 소멸

생성자에서 무거운 초기화 작업을 하면 안 되는 이유가 여기 있어요. 생성자 호출 시점에는 아직 의존관계 주입이 끝나지 않았기 때문입니다. 초기화 로직은 @PostConstruct, 정리 로직은 @PreDestroy에 두는 것이 권장 방식입니다.

AOP (Aspect Oriented Programming)

로깅, 트랜잭션, 보안처럼 여러 클래스에 흩어지는 공통 관심사(cross-cutting concern)를 분리해서 한 곳에 모으는 프로그래밍 방식입니다.

Aspect : 공통 기능 모듈  |  Advice : 실제 부가 기능 로직  |  Pointcut : 어디에 적용할지 지정하는 표현식

JoinPoint : 적용 가능한 지점  |  Target : 적용 대상 객체  |  Weaving : 부가 기능을 연결하는 과정

스프링 AOP는 런타임에 프록시 객체를 만들어 적용합니다. 인터페이스가 있으면 JDK 동적 프록시, 없으면 CGLIB를 사용하는데 스프링 부트는 기본적으로 CGLIB를 씁니다.

내부 호출에는 AOP가 걸리지 않는다

외부에서 methodB 호출 호출자 → 프록시 → methodB 부가 기능 적용 methodA 안에서 methodB 호출프록시→실제 객체methodAthis→methodB프록시를 거치지 않아 미적용
@Service
public class OrderService {

    public void outer() {   // 프록시를 거쳐 들어온 호출
        inner();            // this.inner()로 직접 호출되어 프록시를 건너뜀
    }

    @Transactional
    public void inner() {   // 애노테이션이 붙어 있어도 트랜잭션이 시작되지 않음
        // ...
    }
}

꼬리 질문 : 같은 클래스 안에서 메서드를 호출하면 AOP가 왜 동작하지 않나요?

내부 호출은 프록시를 거치지 않고 this로 실제 객체의 메서드를 직접 호출하기 때문입니다. 이것을 self-invocation 문제라고 하고, 별도 클래스로 분리하거나 자기 자신을 프록시로 주입받아 해결합니다.

Spring MVC 요청 흐름

요청 처리 클라이언트 → DispatcherServlet → HandlerMapping → HandlerAdapter → Controller 응답 처리ModelAndView→DispatcherServlet→ViewResolver→View 렌더링→클라이언트@RestController를 쓰면 ViewResolver 단계 없이 MessageConverter가 JSON으로 직렬화해 바로 응답합니다.

컨트롤러와 파라미터 바인딩

@Controller는 뷰 이름을 반환하고, @RestController는 @Controller + @ResponseBody라서 객체를 JSON으로 직렬화해 응답 본문에 바로 씁니다.

@RequestParam : 쿼리 스트링이나 폼 데이터  |  @PathVariable : URL 경로의 값  |  @RequestBody : 요청 본문의 JSON을 객체로 변환

Filter, Interceptor, AOP 비교

구분 실행 위치 주요 용도
Filter 서블릿 컨테이너 레벨, DispatcherServlet 이전 인코딩, 전역 로깅, XSS 방어
Interceptor 스프링 컨텍스트 레벨, 컨트롤러 전후 인증, 인가, 핸들러 정보가 필요한 처리
AOP 메서드 레벨 트랜잭션, 비즈니스 로직의 공통 처리

꼬리 질문 : 인증 처리는 Filter와 Interceptor 중 어디서 하나요?

스프링 시큐리티처럼 전역적이고 서블릿 레벨에서 걸러야 하면 Filter, 어떤 컨트롤러와 메서드가 호출되는지 알아야 하는 세밀한 인가 처리면 Interceptor가 적합합니다.

Spring과 Spring Boot

Spring은 XML이나 자바 설정을 직접 작성하고 WAS에 배포해야 했습니다. Spring Boot는 자동 설정과 내장 WAS를 제공해서 설정 부담을 크게 줄였어요.

@SpringBootApplication은 세 가지 애노테이션을 합친 것입니다.

@SpringBootConfiguration : 설정 클래스임을 표시  |  @EnableAutoConfiguration : 자동 설정 활성화  |  @ComponentScan : 해당 패키지 하위 컴포넌트 스캔

자동 설정 원리 : META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 파일에 등록된 설정 클래스들을 읽어옵니다. 각 설정은 @ConditionalOnClass, @ConditionalOnMissingBean 같은 조건부 애노테이션이 붙어 있어서, 클래스패스에 해당 라이브러리가 있고 사용자가 직접 등록한 빈이 없을 때만 적용됩니다.

Starter 의존성 : spring-boot-starter-web 하나만 추가하면 필요한 라이브러리 묶음과 버전 호환성까지 함께 관리됩니다.

트랜잭션

@Transactional은 메서드 실행을 하나의 트랜잭션으로 묶습니다. 프록시가 메서드 시작 전에 트랜잭션을 열고, 정상 종료되면 커밋, 예외가 발생하면 롤백해요.

전파 속성(Propagation) : 기본값은 REQUIRED. 진행 중인 트랜잭션이 있으면 참여하고, 없으면 새로 만듭니다. REQUIRES_NEW는 항상 별도 트랜잭션을 만들어서 로그 기록처럼 주 작업의 롤백과 분리하고 싶을 때 씁니다.

readOnly = true : 조회 전용 메서드에 지정하면 JPA에서는 플러시와 스냅샷 비교를 생략하고, DB 드라이버에 따라서는 읽기 전용 최적화가 걸립니다.

꼬리 질문 : 어떤 예외에서 롤백되나요?

기본적으로 RuntimeException과 Error에서만 롤백됩니다. IOException 같은 체크 예외는 롤백되지 않기 때문에, 필요하면 rollbackFor = Exception.class로 명시해야 합니다.

꼬리 질문 : @Transactional이 동작하지 않는 경우가 있나요?

private 메서드에는 프록시를 적용할 수 없어서 동작하지 않습니다. 같은 클래스 내부에서 호출하는 경우에도 프록시를 거치지 않으므로 동작하지 않아요. 5번의 AOP와 같은 이유입니다.