- construction-injectiondependency-injectionjavalombokMVCspring-framework
순수 자바 기반 방식과 Spring 생성자 주입 방식의 MVC 구조 비교
순수 자바 기반 방식과 Spring 생성자 주입 방식의 MVC 모델
MVC 모델에서는 Controller 객체가 Service 객체를 필드로 가지고, Service 객체는 Repository 객체를 필드로 가진다.
웹 요청이 들어오면 해당 요청을 처리하는 Controller가 실행되고, Controller는 Service를 호출하며 Service는 Repository를 통해 데이터에 접근하고 처리한다.
이 과정에서 객체 간 의존성이 자연스럽게 발생한다. 순수 자바에서도 이런 의존성을 분리하는 코드를 만들 수 있지만, Spring을 사용하면 의존성 주입을 프레임워크가 처리해 주기 때문에 개발이 훨씬 편해진다.
여기서는 순수 자바 기반 방식과 Spring의 생성자 주입 방식에서 MVC 구조의 코드 의존성을 비교해 본다.
-
Repository: 데이터베이스와의 직접적인 통신을 담당한다.
-
Service: 비즈니스 로직을 처리한다.
-
Controller: 사용자 요청을 받아 Service를 호출한다.
순수 자바와 Spring 같 MVC 구조 비교
스프링 없이 구현할 때 (순수 자바 기반)
순수 자바에서도 인터페이스를 사용해 의존성을 분리할 수 있다. 하지만 객체 생성과 의존성 연결은 여전히 개발자가 직접 new 키워드로 관리해야 한다. 이 구조에서는 다음과 같은 특징이 있다.
- 객체 생성 책임이 클래스 내부에 존재한다.
- 상위 계층이 하위 계층 구현에 직접 의존한다.
- 구현체 교체와 테스트가 어렵다.
// 인터페이스 정의
interface MemberRepository {
void save(String name);
}
interface MemberService {
void join(String name);
}
// Repository: 데이터 저장소
class MemoryMemberRepository implements MemberRepository {
public void save(String name) { System.out.println(name + " 저장"); }
}
// Service: 비즈니스 로직 (Repository를 직접 생성)
class MemberServiceImpl implements MemberService {
private MemberRepository repository = new MemberRepository(); // 직접 생성
public void join(String name) { repository.save(name); }
}
// Controller: 요청 처리 (Service를 직접 생성)
class MemberController {
private MemberService service = new MemberService(); // 직접 생성
public void create() { service.join("홍길동"); }
}스프링에서 생성자 주입(DI)을 사용할 때
Spring을 사용하면 클래스에 Annotation을 붙여 Spring Bean으로 등록한다. 객체 생성과 의존성 주입은 Spring IoC Container가 담당한다. 또한 인터페이스를 사용하면 실행 시점에 구현체를 교체할 수 있어 구조가 더 유연해진다. 주요 Annotation의 역할은 다음과 같다.
- @Repository: 데이터 접근 계층임을 나타내며 데이터 접근 예외를 Spring의 예외로 변환한다.
- @Service: 비즈니스 로직 계층을 나타낸다.
- @Controller: 웹 요청을 처리하는 컨트롤러를 등록한다.
- @Autowired: Spring이 관리하는 Bean을 자동으로 연결한다.
이 구조에서는 객체 생성과 의존성 연결을 Spring이 담당한다. 개발자는 객체 생성 대신 비즈니스 로직 구현에 집중할 수 있다.
// --- 인터페이스 정의 (결합도 낮춤) ---
interface MemberService { void join(String name); }
interface MemberRepository { void save(String name); }
// --- 구현체 및 주입 ---
@Repository
public class MemoryMemberRepository implements MemberRepository {
public void save(String name) { System.out.println(name + " 저장"); }
}
@Service
public class MemberServiceImpl implements MemberService {
private final MemberRepository repository;
// @Autowired // 생성자가 하나면 스프링이 자동으로 붙여줌 (생략 가능)
public MemberServiceImpl(MemberRepository repository) {
this.repository = repository;
}
public void join(String name) { repository.save(name); }
}
@Controller
public class MemberController {
private final MemberService memberService;
// @Autowired // 이 어노테이션이 있으면 스프링이 컨테이너에서 빈을 찾아 주입함
public MemberController(MemberService memberService) {
this.memberService = memberService;
}
public void create() { memberService.join("홍길동"); }
}
요약 비교
-
스프링 미사용
인터페이스를 사용해 구조를 분리할 수 있지만 객체 생성과 연결을new로 직접 관리해야 한다. -
스프링 생성자 주입
객체 생성과 의존성 연결을 Spring이 담당한다. 이를 통해 제어의 역전(IoC)이 발생하며 구조가 유연해진다. 또한 필드를final로 선언할 수 있어 객체 불변성을 유지할 수 있고 테스트 코드 작성도 쉬워진다.
Lombok(@RequiredArgsConstructor)을 적용한 Spring 코드
Lombok의 @RequiredArgsConstructor를 사용하면 final이 붙은 필드를 기반으로 생성자를 자동 생성한다. 따라서 개발자가 직접 생성자를 작성할 필요가 없어 코드가 간결해진다.
interface MemberRepository { void save(String name); }
interface MemberService { void join(String name); }
// 1. Repository (Lombok 필요 없음)
@Repository
public class MemoryMemberRepository implements MemberRepository {
public void save(String name) { System.out.println(name + " 저장"); }
}
// 2. Service (Lombok 적용)
@Service
@RequiredArgsConstructor // final 필드에 대한 생성자를 자동으로 생성
public class MemberServiceImpl implements MemberService {
private final MemberRepository repository; // 생성자 주입
// @RequiredArgsConstructor가 아래 생성자를 대신 만들어줌:
// public MemberService(MemberRepository repository) { this.repository = repository; }
public void join(String name) { repository.save(name); }
}
// 3. Controller (Lombok 적용)
@Controller
@RequiredArgsConstructor // final 필드인 service를 주입하는 생성자 자동 생성
public class MemberController {
private final MemberService service; // 생성자 주입
public void create() { service.join("홍길동"); }
}Lombok을 사용하면 다음과 같은 장점이 있다.
-
코드 간결화
필드가 늘어나도 생성자를 직접 수정할 필요가 없다. -
가독성 향상
반복적인 생성자 코드가 사라져 클래스 구조가 더 명확하게 보인다. -
생성자 주입 유지
코드가 짧아지더라도 내부적으로는 동일한 생성자 주입 구조이므로 객체 불변성과 테스트 용이성을 유지할 수 있다. -
컴파일 타임 체크
final필드에 값이 전달되지 않으면 컴파일 에러가 발생한다. -
수정 용이성
새로운 의존성이 추가될 때 필드에final만 추가하면 된다. -
의존성 가시성
클래스 상단의final필드를 통해 필요한 의존성을 한눈에 확인할 수 있다. -
테스트 용이성
Spring 없이도new OrderController(new MockService())형태로 테스트할 수 있다.
(끝)