오늘은 현대 자바 개발의 표준이라 불리는 Spring Boot의 심장, DI(Dependency Injection)와 계층 구조(Layered Architecture)에 대해 심도 있게 다뤄보겠다.
스프링을 관통하는 가장 중요한 개념은 제어의 역전(Inversion of Control, IoC)과 의존성 주입(Dependency Injection, DI)이다.
객체가 다른 객체를 참조할 때, 본인이 직접 new 키워드로 생성하지 않고 외부(Spring Container)로부터 주입받는 디자인 패턴이다.
탈피하는 강한 결합: 직접 값을 할당(Assign)하지 않으므로 코드 변경이 용이하다.
유연성 확보: 클래스 간의 결합도가 낮아져 유지보수가 쉬워진다.
재사용성과 테스트: 객체를 갈아 끼우기 편리하여 테스트 코드 작성이 용이해진다.
Note: 비즈니스 로직에만 집중하라. 객체 생성과 생명주기 관리는 스프링이 한다.
스프링에서 빈(Bean)을 주입하는 방법은 크게 세 가지가 있다.
생성자 주입 (Constructor Injection): 가장 권장되는 방식이다. 객체 생성 시점에 의존성을 주입하므로 '불변성'을 보장하며, 테스트 코드 작성 시 의존성 누락을 컴파일 단계에서 방지할 수 있다.
Setter 주입: 주입받는 객체가 변경될 가능성이 있는 경우 사용한다.
필드 주입 (Field Injection): @Autowired만 붙이면 되어 가장 간단하지만, 외부에서 변경이 어렵고 프레임워크에 의존적이라 테스트가 어렵다는 단점이 있다.
효율적인 역할 분담을 위해 스프링 부트는 보통 다음과 같은 흐름을 따른다.
유저 ↔ Controller ↔ Service ↔ DAO(Repository) ↔ Database
Controller: 사용자의 브라우저 요청을 가장 먼저 받는 관문이다. 요청을 해석하고 적절한 서비스로 넘긴다.
Service: 실제 업무(Business) 로직을 수행한다. 보통 인터페이스를 먼저 만들고, 이를 구현한 ServiceImpl 클래스를 작성하는 것이 관례다.
DAO/Repository: 데이터베이스와 직접 통신하며 데이터를 핸들링한다.
이제 제공된 예제 코드를 통해 실제 스프링 부트 프로젝트가 어떻게 구성되는지 살펴보자.
먼저 로직의 뼈대가 되는 인터페이스와 구현체를 작성한다. 구현체에는 @Service 어노테이션을 붙여 스프링이 관리하도록 한다.
// HellouService.java (Interface)
package com.ssafy.hellou.service;
public interface HellouService {
void go(); // 업무 로직의 명세
}
// HellouServiceImpl.java (Implementation)
package com.ssafy.hellou.service;
import org.springframework.stereotype.Service;
@Service // 이 객체는 이제 Spring이 관리하는 'Bean'이 된다.
public class HellouServiceImpl implements HellouService {
@Override
public void go() {
System.out.println("go 호출됨!!!");
}
}
사용자의 요청을 받는 컨트롤러다. 여기서는 세 가지 주입 방식 중 생성자 주입을 채택했다.
package com.ssafy.hellou.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import com.ssafy.hellou.service.HellouService;
@Controller // Spring이 이 클래스를 Bean으로 관리하며 자동으로 'new' 해준다.
public class HellouController {
private static final Logger log = LoggerFactory.getLogger(HellouController.class);
private final HellouService service;
// 생성자 주입 방식 (가장 권장됨)
@Autowired // 생성자가 하나일 경우 생략 가능
public HellouController(HellouService service) {
this.service = service;
}
@GetMapping("/test")
public String asdf(Model model) {
log.debug("디버그 메시지 로그 출력");
service.go(); // 주입받은 서비스 객체의 메서드 실행
model.addAttribute("msg", "홍길동");
return "test"; // /WEB-INF/test.jsp로 이동
}
}
뷰 리졸버(View Resolver) 설정과 로그 레벨을 지정한다.
spring.application.name=hellou
# JSP를 사용하기 위한 뷰 경로 설정
spring.mvc.view.prefix=/WEB-INF/
spring.mvc.view.suffix=.jsp
# 로깅 레벨 설정 (com.ssafy 패키지 하위는 debug 레벨까지 출력)
logging.level.com.ssafy=debug
스프링 부트의 핵심은 결국 "관리의 주체를 나에서 프레임워크로 옮기는 것"이다. 빈(Bean)으로 등록된 객체들은 기본적으로 싱글톤(Singleton)으로 동작하여 메모리 효율을 극대화한다.