스파르타클럽 | AI시대, 미래를 돌파하는 힘
누구나 잠재력을 깨워 나아가도록. IT 커리어의 모든 성장 과정을 스파르타클럽에서
academia.spartaclub.kr
🧩 3 Layer Architecture
현재 메모장 프로젝트의 문제점
- Controller 클래스 하나로 모든 API를 처리하고 있음
- 현재는 API 수가 적고 기능이 단순하여 코드가 복잡해 보이지 않을 수 있지만 앞으로 기능이 추가되고 복잡해진다면 문제가 발생할 수 있음
- 한 개의 클래스에 너무 많은 양의 코드가 존재하기 때문에 코드를 이해하기 어려움
- 현업에서는 코드의 추가 혹은 변경 요청이 계속 생길 수 있음
- 문제가 발생했는데 해당 Controller 클래스를 구현한 개발자가 퇴사한다면???
Spring의 3 Layer Architecture
- Controller
- 클라이언트의 요청을 받음
- 요청에 대한 로직 처리는 Service에게 전달함
- Request 데이터가 있으면 Service에 같이 전달함
- Serivce에서 처리 완료된 결과를 클라이언트에게 응답함
- Service
- 사용자의 요구사항을 처리하는 비즈니스 로직
- 현업에서는 계속해서 비대해지는 부분
- DB 저장 및 조회가 필요할 때는 Repository에게 요청함
- Repository
- DB 관리 및 CRUD 작업을 처리함
역할 분리하기
- Controller → Service 분리: MemoController, MemoService
- Service → Repository 분리: MemoService, MemoRepository
🧩 IoC(제어의 역전)와 DI(의존성 주입)
Spring의 IoC와 DI
- IoC, DI는 객체지향의 SOLID 원칙과 GoF의 디자인 패턴과 같은 설계 원칙 및 디자인 패턴
- IoC는 설계 원칙에 해당 (예: 김치 볶음밤을 맛있게 만드는 방법)
- DI는 디자인 패턴에 해당 (예: 김치 볶음밥 레시피)
의존성
public class Consumer {
void eat() {
Chicken chicken = new Chicken();
chicken.eat();
}
public static void main(String[] args) {
Consumer consumer = new Consumer();
consumer.eat();
}
}
class Chicken {
public void eat() {
System.out.println("치킨을 먹는다.");
}
}
- 위 코드에서 Consumer와 Chicken은 강하게 결합되어 있음
- Consumer가 치킨이 아니라 피자가 먹고 싶다면 많은 수의 코드 변경이 필요함
- 이러한 관계를 강한 결합 및 강한 의존성이라고 함
public class Consumer {
void eat(Food food) {
food.eat();
}
public static void main(String[] args) {
Consumer consumer = new Consumer();
consumer.eat(new Chicken());
consumer.eat(new Pizza());
}
}
interface Food {
void eat();
}
class Chicken implements Food{
@Override
public void eat() {
System.out.println("치킨을 먹는다.");
}
}
class Pizza implements Food{
@Override
public void eat() {
System.out.println("피자를 먹는다.");
}
}
- Java의 Interface를 활용하면 이를 해결할 수 있음
- Interface 다형성의 원리를 사용해서 구현하면 고객이 어떤 음식을 요구해도 쉽게 대처 가능함
- 이러한 관계를 약한 결합 및 약한 의존성이라고 함
주입
- 여러 방법을 통해 필요로 하는 객체를 해당 객체에 전달하는 것
- 필드에 직접 주입: Food를 Consumer에 포함시킴
public class Consumer {
Food food;
void eat() {
this.food.eat();
}
public static void main(String[] args) {
Consumer consumer = new Consumer();
consumer.food = new Chicken();
consumer.eat();
consumer.food = new Pizza();
consumer.eat();
}
}
interface Food {
void eat();
}
class Chicken implements Food{
@Override
public void eat() {
System.out.println("치킨을 먹는다.");
}
}
class Pizza implements Food{
@Override
public void eat() {
System.out.println("피자를 먹는다.");
}
}
- 메서드를 통한 주입: set 메서드 사용
public class Consumer {
Food food;
void eat() {
this.food.eat();
}
public void setFood(Food food) {
this.food = food;
}
public static void main(String[] args) {
Consumer consumer = new Consumer();
consumer.setFood(new Chicken());
consumer.eat();
consumer.setFood(new Pizza());
consumer.eat();
}
}
interface Food {
void eat();
}
class Chicken implements Food{
@Override
public void eat() {
System.out.println("치킨을 먹는다.");
}
}
class Pizza implements Food{
@Override
public void eat() {
System.out.println("피자를 먹는다.");
}
}
- 생성자를 통한 주입
public class Consumer {
Food food;
public Consumer(Food food) {
this.food = food;
}
void eat() {
this.food.eat();
}
public static void main(String[] args) {
Consumer consumer = new Consumer(new Chicken());
consumer.eat();
consumer = new Consumer(new Pizza());
consumer.eat();
}
}
interface Food {
void eat();
}
class Chicken implements Food{
@Override
public void eat() {
System.out.println("치킨을 먹는다.");
}
}
class Pizza implements Food{
@Override
public void eat() {
System.out.println("피자를 먹는다.");
}
}
제어의 역전
- 이전에는 Consumer가 직접 Food를 만들어 먹었기 때문에 새로운 Food를 만들려면 추가적인 코드 변경이 불가피했음
- 제어의 흐름: Consumer → Food
- 이를 해결하기 위해 만들어진 Food를 Consumer에게 전달해주는 식으로 변경함으로써 Consumer는 추가적인 코드 변경 없이 어느 Food가 되었든지 전부 먹을 수 있게 되었음
- 제어의 흐름: Food → Consumer으로 역전됨
메모장 프로젝트의 IoC와 DI
- MemoService: 객체 중복 생성 문제 해결
/* 수정 전 */
public class MemoService {
private final JdbcTemplate jdbcTemplate;
public MemoService(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public MemoResponseDto createMemo(MemoRequestDto requestDto) {
MemoRepository memoRepository = new MemoRepository(jdbcTemplate);
...
}
public List<MemoResponseDto> getMemos() {
MemoRepository memoRepository = new MemoRepository(jdbcTemplate);
...
}
public Long updateMemo(Long id, MemoRequestDto requestDto) {
MemoRepository memoRepository = new MemoRepository(jdbcTemplate);
...
}
public Long deleteMemo(Long id) {
MemoRepository memoRepository = new MemoRepository(jdbcTemplate);
...
}
}
/* 수정 후 */
public class MemoService {
private final MemoRepository memoRepository;
public MemoService(JdbcTemplate jdbcTemplate) {
this.memoRepository = new MemoRepository(jdbcTemplate);
}
public MemoResponseDto createMemo(MemoRequestDto requestDto) {
Memo memo = new Memo(requestDto);
Memo saveMemo = memoRepository.save(requestDto);
MemoResponseDto memoResponseDto = new MemoResponseDto(saveMemo);
return memoResponseDto;
}
...
}
- 제어의 흐름: Controller → Service → Repository (강한 결합)
- Controller가 Service를 직접 만들고 Service도 Repository를 직접 만듦
- Repository1 생성자 변경에 의해 모든 Controller와 모든 Service의 코드 변경이 필요함
- 강한 결합 해결책
- 각 객체에 대한 객체 생성은 1번씩만
- 생성된 객체를 모든 곳에서 재사용
- 생성자 주입을 사용해 필요로 하는 객체 주입
- MemoController 수정: 만들어진 memoService를 받아오도록
public class MemoController {
private final MemoService memoService;
public MemoController(MemoService memoService) {
this.memoService = memoService;
}
/* ... */
}
- MemoService 수정: 만들어진 memoRepository를 받아오도록
public class MemoService {
private final MemoRepository memoRepository;
public MemoService(MemoRepository memoRepository) {
this.memoRepository = memoRepository;
}
/* ... */
}
- 외부에서 미리 만든 객체를 주입하는 것이 DI 패턴이라고 이해했고 그렇게 코드를 수정했는데, 그렇다면 도대체 MemoRepository 등의 객체들은 언제 어디서 누가 만들어주고 넣어주는 거지?

🧩 IoC Container와 Bean
- DI를 사용하기 위해서는 객체 생성이 우선 되어야 한다. 그렇다면 언제 어디서 누가 객체를 생성하는가?
👉 Spring 프레임워크가 필요한 객체를 생성하고 관리하는 역할을 대신 해준다.- Bean: Spring이 관리하는 객체
- Spring IoC Container: Bean을 모아둔 컨테이너
Spring Bean 등록 방법
- @Component
@Component
public class MemoService {
/* ... */
}
@Component
public class MemoRepository {
/* ... */
}
- @ComponentScan: Spring Boot 애플리케이션이 자체적으로 @Component 애노테이션이 붙은 클래스들을 Bean으로 등록한다.

- 코드 좌측에 초록색 커피콩 아이콘이 있다면 Bean으로 등록이 되었다는 뜻이다.

- Bean을 사용하려면 @Autowired을 달아줘야 한다.
- Spring 4.3 버전부터 생성자가 하나만 선언한 경우 생략 가능함
- 주입이 잘 된 경우 커피콩 아이콘 위에 화살표가 붙음

- 메서드로 주입하는 방법
- memoRepository가 final이 될 수 없고, @Autowired 애노테이션을 반드시 붙여야 Bean으로 사용 가능함
- private임에도 불구하고 Spring에 의해 외부에서 Bean 객체를 주입 가능함
- 하지만 객체 불변성을 지키기 위해 현업에서는 거의 생성자 주입 방식을 사용함
@Component
public class MemoService {
private MemoRepository memoRepository;
// final 사용 불가
@Autowired
public void setDi(MemoRepository memoRepository) {
this.memoRepository = memoRepository;
}
/* ... */
}
- Lombok 사용: @RequiredArgsConstructor가 final로 선언된 멤버 변수를 파라미터로 사용하여 생성자를 자동으로 생성함
@Component
@RequiredArgsConstructor
public class MemoService {
private final MemoRepository memoRepository;
/* ... */
}
- @Autowired는 Spring IoC Container에 의해서 관리되고 있는 Bean class에서만 사용 가능함
ApplicationContext
- BeanFactory 등을 상속하여 기능을 확장한 Container
- BeanFactory: Bean의 생성, 관계 설정 등의 제어를 담당하는 IoC 객체
- 스프링 IoC 컨테이너에서 Bean을 수동으로 가져오는 방법
@Component
public class MemoService {
private final MemoRepository memoRepository;
public MemoService(ApplicationContext context) {
// 1.'Bean' 이름으로 가져오기
MemoRepository memoRepository = (MemoRepository) context.getBean("memoRepository");
// 2.'Bean' 클래스 형식으로 가져오기
// MemoRepository memoRepository = context.getBean(MemoRepository.class);
this.memoRepository = memoRepository;
}
/* ... */
}
3 Layer Annotation
- Controller, Service, Repository의 역할로 구분된 클래스들을 Bean으로 등록할 때 해당 Bean 클래스의 역할을 명시하기 위해 사용됨
- @Controller, @RestController
- @Service
- @Repository
'내일배움캠프' 카테고리의 다른 글
| [내일배움캠프] Bean 수동 등록, 쿠키와 세션 (0) | 2026.04.07 |
|---|---|
| [내일배움캠프] JPA와 Entity, 영속성 컨텍스트 (0) | 2026.04.06 |
| [내일배움캠프] Spring 입문 1주차 (0) | 2026.04.06 |
| [내일배움캠프 사전캠프] 동시성 이슈 해결하기 (0) | 2026.04.03 |
| [내일배움캠프 사전캠프] 페이지네이션과 N+1 문제 (0) | 2026.04.02 |