새로운 내용을 공부할 때
새로운 내용의 공부를 시작할 때 용어의 정의를 이해하지 못하거나 정확하게 알지 못한다면 그 용어가 포함된 문장을 이해하지 못합니다.
작은 단어 하나가 내용을 이해하지 못하게 하기 때문에 용어를 정확하게 이해하는 것이 중요합니다.
HTTP API 테스트와 HelloController 설명 보충
HTTP API 테스트와 HelloController 설명 보충
목차
• ComponentScan과 @Configuration
위 학습 목표를 달성하기 위해 스프링 IoC, DI, 템플릿 메소드 패턴, Bean 생명주기, ComponentScan 등의 개념을 단계별로 정리하였습니다.
스프링 IoC와 DispatcherServlet
1. 스프링 IoC(Inversion of Control)
-
IoC 는 전통적인 방식에서 애플리케이션 코드가 객체를 생성하고 관리하던 흐름을 역전시킵니다.
-
스프링 컨테이너(ApplicationContext)가 객체(빈, Bean)들을 대신 생성 및 관리하며, 필요한 시점에 의존성을 주입해줍니다.
2. DispatcherServlet
• 스프링 MVC에서의 Front Controller 역할을 담당하는 서블릿입니다.
• 모든 웹 요청을 받아서 스프링 컨테이너 안에서 매핑된 컨트롤러(예: @Controller, @RestController)를 찾아 위임합니다.
• 매핑, 바인딩, 예외 처리 등의 부수적인 작업을 내부적으로 처리하여 개발자는 비즈니스 로직에만 집중할 수 있게 해줍니다.
간단 예시 코드
ServletWebServerFactory serverFactory = new TomcatServletWebServerFactory();
WebServer webServer = serverFactory.getWebServer(servletContext -> {
// DispatcherServlet 생성 및 스프링 컨테이너 연결
servletContext.addServlet(
"dispatcherServlet",
new DispatcherServlet(applicationContext) // WebApplicationContext 필요
).addMapping("/*");
});
webServer.start();
• DispatcherServlet은 스프링 컨테이너(예: AnnotationConfigServletWebApplicationContext)와 연결되어
요청 처리에 필요한 Controller 및 Service를 찾아서 호출합니다.
자바 코드 구성 정보와 의존성 주입(DI)
1. 자바 코드로 구성 정보를 등록하는 이유
• XML 기반 설정에서 벗어나 가독성과 유지보수성을 높이기 위함
• 팩토리 메소드(@Bean)를 활용해 의도를 드러내는 방식으로 객체를 생성
• 필요한 시점에 생성 로직이나 의존 관계를 자유롭게 커스터마이징 가능
2. DI(Dependency Injection) 방식
1. 생성자 주입
2. Setter 주입
3. 필드 주입(권장되지 않음. 테스트·확장성 측면에서 불리)
예시 코드: 팩토리 메소드 방식
@Configuration
public class HelloApplication {
// DispatcherServlet과 Tomcat 설정(스프링 부트에서는 자동화됨)
@Bean
public ServletWebServerFactory servletWebServerFactory() {
return new TomcatServletWebServerFactory();
}
@Bean
public DispatcherServlet dispatcherServlet() {
return new DispatcherServlet();
}
// Controller와 Service를 스프링 빈으로 등록
@Bean
public HelloService helloService() {
return new SimpleHelloService();
}
@Bean
public HelloController helloController(HelloService helloService) {
// 생성자 주입
return new HelloController(helloService);
}
}
• @Configuration 클래스는 스프링 컨테이너가 참조할 구성 정보를 담고 있습니다.
• @Bean 메서드들은 각각 스프링 빈을 생성하고, 의존성을 연결합니다.
• 다른 빈에서 HelloService helloService와 같이 인자로 받아 주입받는 방식(생성자 주입)을 쓰면,
테스트, 확장 시에 보다 편리합니다(의존 대상 교체 등).
ComponentScan과 @Configuration
1. @ComponentScan
• @ComponentScan이 선언된 패키지와 그 하위 패키지에서 @Component(메타 애노테이션 포함)
가 붙은 클래스를 자동으로 스프링 빈으로 등록합니다.
• @Controller, @Service, @Repository 등은 모두 @Component를 메타 애노테이션으로 갖고 있어
자동 스캐닝 대상이 됩니다.
@Configuration
@ComponentScan
public class HelloApplication {
// 톰캣 서버 및 DispatcherServlet Bean 등록
@Bean
public ServletWebServerFactory servletWebServerFactory() {
return new TomcatServletWebServerFactory();
}
@Bean
public DispatcherServlet dispatcherServlet() {
return new DispatcherServlet();
}
public static void main(String[] args) {
AnnotationConfigServletWebApplicationContext applicationContext
= new AnnotationConfigServletWebApplicationContext() {
@Override
protected void onRefresh() {
super.onRefresh();
// 서블릿 컨테이너 등록 및 실행
ServletWebServerFactory serverFactory = this.getBean(ServletWebServerFactory.class);
DispatcherServlet servlet = this.getBean(DispatcherServlet.class);
WebServer webServer = serverFactory.getWebServer(servletContext -> {
servletContext.addServlet("dispatcherServlet", servlet).addMapping("/*");
});
webServer.start();
}
};
// 구성 정보 등록
applicationContext.register(HelloApplication.class);
applicationContext.refresh();
}
}
• 이렇게 HelloApplication 클래스에 @ComponentScan이 붙어 있으면,
동일 패키지(또는 지정된 패키지) 하위에 @Controller, @Service, @Repository 등을 붙여 놓기만 해도
알아서 스캔 및 빈 등록이 이루어집니다.
2. @Configuration과 @ComponentScan의 차이
• @Configuration 클래스는 직접 빈을 정의(@Bean)하거나 특정 설정 정보를 담아 놓을 때 사용합니다.
• @ComponentScan은 지정된 패키지 이하의 빈을 자동으로 검색해서 등록합니다.
• 일반적으로 @SpringBootApplication에는 내부적으로 @Configuration, @EnableAutoConfiguration,
@ComponentScan이 모두 포함되어 있습니다.
Bean 생명주기와 Hook 메소드
1. refresh()와 템플릿 메소드 패턴
• applicationContext.refresh()를 호출하면 스프링 컨테이너 초기화가 진행됩니다.
• 내부적으로 템플릿 메소드(onRefresh()) 등이 호출되어 여러 단계(Bean 팩토리 초기화 → 후처리기 등록 → Bean 생성)가 진행됩니다.
@Override
protected void onRefresh() {
super.onRefresh();
// 서블릿 웹 서버를 띄운다거나, 추가 작업을 여기서 진행할 수 있음
}
• Hook 메소드: 부모 클래스(AbstractApplicationContext)의 템플릿 메소드(refresh())가 호출되는 과정에서
자식 클래스가 특정 지점에 개입(onRefresh 등)하여 확장할 수 있게 만드는 구조입니다.
2. Bean 생명주기 콜백
• Bean 객체가 생성된 후 @PostConstruct, InitializingBean.afterPropertiesSet() 같은
초기화 메소드가 호출될 수 있습니다.
• DisposableBean.destroy() 등으로 소멸 메소드를 활용할 수도 있습니다.
• ApplicationContextAware 등을 구현하면, 컨테이너(스프링 컨텍스트)를 주입받을 수도 있습니다.
객체지향 설계(SOLID) 적용 사례
1. SRP(Single Responsibility Principle)
• Controller는 웹 요청을 받아 검증 및 결과 반환 형식 처리에만 집중
• Service는 비즈니스 로직 실행에 집중
• Repository는 데이터 엑세스(DB 접근)에 집중
• 역할을 명확히 분리하면 유지보수성과 확장성이 높아집니다.
2. DIP(Dependency Inversion Principle)
• 추상(인터페이스)에 의존하도록 구조를 설계
• 구체 클래스(예: SimpleHelloService)가 아니라 HelloService 인터페이스에 의존
• 스프링 DI를 통해서 런타임에 적절한 구현체를 주입받게 만듦
간단 예시 코드
public interface HelloService {
String sayHello(String name);
}
@Service
public class SimpleHelloService implements HelloService {
@Override
public String sayHello(String name) {
return "Hello, " + name;
}
}
@RestController
public class HelloController {
private final HelloService helloService;
// 생성자 주입
public HelloController(HelloService helloService) {
this.helloService = helloService;
}
@GetMapping("/hello")
public String hello(String name) {
if (name == null) throw new IllegalArgumentException("이름이 비어있습니다.");
return helloService.sayHello(name);
}
}
트랜잭션 관리와 트레이드오프
1. 스프링 트랜잭션 관리
• @Transactional 애노테이션을 활용해 ACID 속성을 보장(원자성, 일관성, 고립성, 지속성)
• Isolation Level(격리 수준)을 지정해 동시성 이슈를 제어할 수 있음
• Propagation 속성으로 트랜잭션 전파 방식을 정의할 수 있음
2. 트레이드오프(트랜잭션 범위 설정)
선택 사항 | 장점 | 단점 |
---|---|---|
넓은 범위의 트랜잭션 | 일관성(Consistency)을 최대한 보장 | 오랜 시간 잠금이 걸릴 수 있어 성능 저하 우려 |
좁은 범위(메소드 단위) 트랜잭션 | 병렬 처리나 성능에 유리 | 여러 트랜잭션 간의 중간 상태가 발생할 가능성 |
• MSA 환경에서는 분산 트랜잭션을 위해 SAGA 패턴 등이 사용됨.
하지만 분산 락 등의 복잡성이 발생하므로, 비즈니스 요구사항에 따라 신중히 결정해야 합니다.
실무 적용 시 고려사항
1. Spring Boot 사용
• 톰캣, 제티 등의 서블릿 컨테이너 자동 설정
• @SpringBootApplication(내부적으로 @EnableAutoConfiguration, @ComponentScan)을 통해
필요한 Bean 자동 등록 및 설정
2. 객체지향 설계 vs 성능
• 지나치게 세분화된 구조(클래스, 인터페이스 남발)는 초기 개발 속도 저하
• 그러나 대규모 시스템에서는 유지보수성, 확장성을 확보하는 편이 궁극적으로 이득
3. 테스트
• DI 구조에서는 Mock(가짜 구현체)로 교체가 쉬워져 단위 테스트가 용이
• 통합 테스트 시에는 실제 DB, 메시지 브로커, 외부 서비스 연동도 Spring Test 지원 기능으로 수월
4. 트랜잭션 범위
• @Transactional을 클래스 단위로 걸어둘지 메소드 단위로 할지 고민 필요
• 실제 비즈니스 로직의 트랜잭션 경계를 정확히 파악해 불필요한 Lock으로 인한 성능 저하를 방지
추가 인터뷰 질문
1. Spring의 IoC(Inversion of Control)은 전통적인 자바 객체 관리와 비교했을 때 어떤 장점이 있나요?
2. SOLID 원칙 중 DIP(Dependency Inversion Principle)를 스프링 DI와 연관지어 설명한다면?
3. 트랜잭션 격리 수준(Isolation Level)의 설정에 따른 장단점은 무엇이고, 어떻게 결정해야 할까요?
4. @ComponentScan과 @Configuration의 차이는 무엇이며, 어떤 상황에서 각각을 활용하나요?
5. 스프링 Bean의 라이프사이클에서 생기는 후처리(Post-Processing)는 실무에서 어떻게 활용할 수 있나요?
결론
위 내용을 종합하면, 스프링 컨테이너를 중심으로 DI와 템플릿 메소드 패턴, Hook 메소드를 적절히 활용하면
비즈니스 로직(POJO)을 깔끔히 유지하면서도, IoC를 통한 객체 관리와 확장성이 뛰어난 구조를 얻을 수 있습니다.
또한, 트랜잭션 관리와 객체지향 설계(SOLID)를 접목시켜 대규모 애플리케이션에서도 효율적인 유지보수가 가능해집니다.
“스프링 부트”를 쓰면 위 설정이 자동화되지만,
내부 동작 원리를 이해하면 디버깅, 최적화, 아키텍처 결정에 큰 도움이 됩니다.
댓글남기기