Spring

Spring 1주차 1회차 학습 정리

join5 2026. 4. 26. 18:29
반응형

주제

Spring IoC / DI / Bean

오늘의 핵심 목표

  • IoC, DI, Bean의 의미를 구분해서 설명할 수 있다.
  • 객체를 직접 new로 생성하는 방식의 문제를 설명할 수 있다.
  • Spring Container가 객체를 생성하고 의존성을 주입하는 흐름을 설명할 수 있다.
  • 생성자 주입을 사용하는 이유를 코드와 함께 설명할 수 있다.

1. IoC란 무엇인가?

IoC는 Inversion of Control의 약자로, 제어의 역전이라는 의미다.

기존 Java 코드에서는 개발자가 직접 객체를 생성하고 연결한다.

MemberRepository repository = new MemoryMemberRepository();
MemberService memberService = new MemberService(repository);

하지만 Spring에서는 객체 생성과 의존관계 연결을 Spring Container가 담당한다.

@Service
public class MemberService {

    private final MemberRepository memberRepository;

    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}

즉, 개발자가 직접 객체를 만들고 연결하던 제어권이 Spring Container로 넘어간다.

정리

IoC는 개발자가 직접 객체를 생성하고 연결하던 제어권을 Spring Container에게 넘기는 것을 말한다.
Spring은 객체 생성, 의존성 연결, 생명주기 관리를 담당하고 개발자는 비즈니스 로직에 집중한다.

2. 객체를 직접 new로 생성하면 생기는 문제

직접 구현체를 생성하는 코드는 다음과 같다.

public class MemberService {

    private final MemberRepository memberRepository = new MemoryMemberRepository();

    public void join(String name) {
        memberRepository.save(name);
    }
}

이 코드는 동작은 하지만 구조적으로 문제가 있다.

문제점

문제 설명
강한 결합 MemberServiceMemoryMemberRepository라는 구체 구현체를 직접 알고 있다.
변경에 취약 구현체를 바꾸면 Service 코드도 수정해야 한다.
테스트 어려움 테스트용 Fake Repository를 주입하기 어렵다.
책임 혼합 Service가 비즈니스 로직뿐 아니라 객체 생성 책임까지 가진다.

여기서 핵심은 new 자체가 항상 나쁘다는 뜻이 아니다.

문제는 Service가 직접 구체 구현체를 생성하면서, 비즈니스 로직과 객체 생성 책임을 함께 가지게 된다는 점이다.

정리

new로 직접 구현체를 생성하면 사용하는 클래스가 구체 구현체에 강하게 결합된다.
그 결과 구현체를 교체할 때 Service 코드까지 수정해야 하고, 테스트용 Fake 객체를 주입하기도 어려워진다.
즉, 변경에 약하고 테스트하기 어려운 구조가 된다.

3. DI란 무엇인가?

DI는 Dependency Injection의 약자로, 의존성 주입이라는 의미다.

객체가 필요한 의존 객체를 직접 생성하지 않고 외부에서 주입받는 방식이다.

public class MemberService {

    private final MemberRepository memberRepository;

    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}

여기서 MemberServiceMemberRepository를 직접 만들지 않는다.

new MemoryMemberRepository()

이 코드가 없다.

대신 생성자를 통해 외부에서 MemberRepository를 전달받는다.

Spring에서는 이 외부 역할을 Spring Container가 담당한다.

정리

DI는 객체가 필요한 의존 객체를 직접 생성하지 않고 외부에서 주입받는 방식이다.
Spring에서는 Container가 Bean을 생성하는 과정에서 생성자 등을 통해 필요한 의존성을 주입해준다.
DI는 IoC를 구현하는 대표적인 방법이다.

4. Bean이란 무엇인가?

Bean은 Spring Container가 생성하고 관리하는 객체다.

예를 들어 다음 클래스들은 Spring Bean으로 등록될 수 있다.

@Service
public class MemberService {
}
@Repository
public class MemoryMemberRepository implements MemberRepository {
}
@RestController
public class MemberController {
}

Spring이 Bean을 관리하면 다음 역할을 수행할 수 있다.

역할 설명
객체 생성 Spring Container가 객체를 생성한다.
의존성 주입 필요한 객체를 자동으로 연결한다.
생명주기 관리 객체의 생성, 초기화, 종료 흐름을 관리한다.
부가 기능 적용 트랜잭션, AOP, 프록시 같은 기능도 Bean을 기반으로 적용할 수 있다.

정리

Bean은 Spring Container가 생성하고 관리하는 객체를 말한다.
Spring은 Bean의 생성, 의존성 주입, 생명주기 관리를 담당한다.
트랜잭션, AOP, 프록시 같은 Spring의 여러 기능도 Bean을 기반으로 동작한다.

5. @Service, @Repository, @Controller의 역할

어노테이션 역할
@Controller / @RestController HTTP 요청 처리, URL 매핑
@Service 비즈니스 로직 처리
@Repository 데이터 접근 로직 처리

예시

@RestController
@RequestMapping("/members")
public class MemberController {

    private final MemberService memberService;

    public MemberController(MemberService memberService) {
        this.memberService = memberService;
    }
}
@Service
public class MemberService {

    private final MemberRepository memberRepository;

    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}
@Repository
public class MemoryMemberRepository implements MemberRepository {
}

정리

@Service는 비즈니스 로직을 담당하는 클래스에 붙인다.
@Repository는 DB 접근, 데이터 저장, 조회 같은 영속성 계층 클래스에 붙인다.
@Controller 또는 @RestController는 HTTP 요청을 받아 URL을 매핑하고 Service를 호출하는 웹 계층 클래스에 붙인다.

6. 생성자 주입을 사용하는 이유

생성자 주입은 다음과 같은 형태다.

@Service
public class MemberService {

    private final MemberRepository memberRepository;

    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}

생성자가 하나뿐인 경우 Spring에서는 @Autowired를 생략해도 생성자 주입이 동작한다.

생성자 주입의 장점

장점 설명
필수 의존성 보장 필요한 의존성이 없으면 객체를 생성할 수 없다.
불변성 유지 final 필드를 사용할 수 있다.
테스트 용이 테스트에서 Fake 객체를 직접 넣을 수 있다.
의존성 명시 생성자만 봐도 필요한 의존성을 알 수 있다.

필드 주입 예시

@Service
public class MemberService {

    @Autowired
    private MemberRepository memberRepository;
}

필드 주입은 편해 보이지만 다음 문제가 있다.

문제 설명
불변성 약함 final을 사용할 수 없다.
테스트 불편 직접 객체를 생성해서 테스트하기 어렵다.
의존성 숨김 클래스가 어떤 의존성을 필요로 하는지 명확하지 않다.

정리

생성자 주입은 필요한 의존성이 없으면 객체를 생성할 수 없기 때문에 의존성 누락을 빠르게 발견할 수 있다.
또한 필드를 final로 선언할 수 있어 불변성을 유지할 수 있고, 테스트에서 Fake 객체를 직접 주입하기 쉽다.
클래스가 필요한 의존성도 생성자를 통해 명확히 드러난다.

7. 같은 타입의 Bean이 여러 개일 때

다음처럼 같은 인터페이스를 구현한 Bean이 2개 있다고 가정한다.

@Repository
public class MemoryMemberRepository implements MemberRepository {
}
@Repository
public class JdbcMemberRepository implements MemberRepository {
}

그리고 Service가 다음처럼 의존한다.

@Service
public class MemberService {

    private final MemberRepository memberRepository;

    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}

이 경우 MemberRepository 타입의 Bean 후보가 2개가 된다.

MemberRepository 타입 Bean 후보

1. memoryMemberRepository
2. jdbcMemberRepository

같은 타입의 Bean 후보가 여러 개 있고, 어떤 Bean을 선택해야 할지 판단할 추가 정보가 없으면 Spring은 주입 대상을 결정하지 못한다.

해결 방법 1. @Primary

@Primary
@Repository
public class JdbcMemberRepository implements MemberRepository {
}

@Primary는 같은 타입의 Bean이 여러 개 있을 때 기본으로 선택할 Bean을 지정한다.

해결 방법 2. @Qualifier

@Service
public class MemberService {

    private final MemberRepository memberRepository;

    public MemberService(@Qualifier("jdbcMemberRepository") MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}

@Qualifier는 특정 Bean 이름을 직접 지정한다.

정리

같은 타입의 Bean 후보가 여러 개 있고 추가 정보가 없으면 Spring은 어떤 Bean을 주입해야 할지 결정하지 못한다.
이때 @Primary로 기본 Bean을 지정하거나, @Qualifier로 특정 Bean 이름을 명시해서 해결할 수 있다.

8. 구체 구현체가 아니라 역할에 의존해야 하는 이유

나쁜 예시는 다음과 같다.

@Service
public class MemberService {

    private final MemoryMemberRepository memberRepository;

    public MemberService(MemoryMemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}

이 코드는 MemberServiceMemoryMemberRepository라는 구체 구현체에 직접 의존한다.

개선된 코드는 다음과 같다.

@Service
public class MemberService {

    private final MemberRepository memberRepository;

    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}

이제 MemberService는 구체 구현체가 아니라 MemberRepository라는 역할에 의존한다.

여기서 MemberRepository는 구현체가 아니라 역할을 표현하는 인터페이스다.

정리

구체 구현체가 아니라 MemberRepository라는 역할에 의존하면 Service는 특정 구현 방식에 덜 묶이게 된다.
따라서 MemoryMemberRepository에서 JdbcMemberRepository로 구현체가 바뀌어도 Service 코드를 수정하지 않아도 된다.
이렇게 역할과 구현을 분리하면 결합도가 낮아지고 변경에 강한 구조가 된다.

9. @SpringBootApplication과 컴포넌트 스캔

Spring Boot 애플리케이션은 보통 다음 코드에서 시작한다.

@SpringBootApplication
public class SpringStudyApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringStudyApplication.class, args);
    }
}

@SpringBootApplication은 Spring Boot 애플리케이션의 시작 설정을 나타내는 편의 어노테이션이다.

내부적으로는 다음 기능을 포함한다.

@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan

이 중 @ComponentScan 덕분에 Spring은 특정 패키지 아래의 클래스를 탐색하면서 Bean으로 등록할 대상을 찾는다.

스캔 대상은 다음과 같다.

@Component
@Service
@Repository
@Controller
@RestController

Spring Boot는 기본적으로 @SpringBootApplication이 붙은 클래스의 패키지를 기준으로 하위 패키지를 스캔한다.

실행 흐름

SpringApplication.run()
→ Spring Container 생성
→ 컴포넌트 스캔
→ Bean 후보 탐색 및 BeanDefinition 등록
→ Bean 생성 시 필요한 의존성 확인
→ 의존 Bean 생성 또는 조회
→ 생성자를 통해 의존성 주입
→ 애플리케이션 실행 준비 완료

여기서 BeanDefinition은 Bean을 어떻게 만들고 관리할지에 대한 메타정보라고 보면 된다.

정리

@SpringBootApplication은 Spring Boot 애플리케이션의 시작 설정을 나타내며, 자동 설정과 컴포넌트 스캔 기능을 포함한다.
Spring Boot는 @SpringBootApplication이 붙은 클래스의 패키지를 기준으로 하위 패키지를 스캔한다.
그 과정에서 @Component, @Service, @Repository, @Controller 등이 붙은 클래스를 Bean 후보로 탐색하고, Bean 생성 과정에서 필요한 의존성을 주입한다.

10. IoC, DI, Bean의 관계

각각의 의미

개념 의미
IoC 객체 생성과 관리의 제어권을 Spring Container에게 넘기는 원칙
DI 객체가 필요한 의존성을 외부에서 주입받는 방식
Bean Spring Container가 생성하고 관리하는 객체

관계

Spring Container
→ Bean 생성 및 관리
→ Bean 사이의 의존성 주입
→ 객체 생명주기 관리

정리

IoC는 객체 생성과 관리의 제어권을 Spring Container에게 넘기는 원칙이다.
DI는 객체가 필요한 의존성을 직접 생성하지 않고 외부에서 주입받는 방식이며, IoC를 구현하는 대표적인 방법이다.
Bean은 Spring Container가 생성하고 관리하는 객체다.

한 문장 정리

Spring은 IoC 원칙에 따라 객체 생성과 관리를 Container가 담당하고, DI 방식으로 Bean들 사이의 의존성을 연결한다.

11. 전체 코드 예시

MemberRepository

public interface MemberRepository {
    void save(String name);
}

MemoryMemberRepository

@Repository
public class MemoryMemberRepository implements MemberRepository {

    @Override
    public void save(String name) {
        System.out.println(name + " 저장");
    }
}

MemberService

@Service
public class MemberService {

    private final MemberRepository memberRepository;

    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    public void join(String name) {
        memberRepository.save(name);
    }
}

MemberController

@RestController
@RequestMapping("/members")
public class MemberController {

    private final MemberService memberService;

    public MemberController(MemberService memberService) {
        this.memberService = memberService;
    }

    @PostMapping
    public String create(@RequestParam String name) {
        memberService.join(name);
        return "created";
    }
}

12. 코드 실행 흐름

HTTP 요청이 들어오면 다음 흐름으로 실행된다.

POST /members?name=park
→ MemberController.create()
→ MemberService.join()
→ MemberRepository.save()
→ MemoryMemberRepository.save()

Spring 내부 준비 흐름은 다음과 같다.

SpringApplication.run()
→ Spring Container 생성
→ @RestController, @Service, @Repository 탐색
→ MemoryMemberRepository Bean 생성
→ MemberService Bean 생성 시 MemberRepository 주입
→ MemberController Bean 생성 시 MemberService 주입
→ 요청 처리 가능 상태

의존관계는 다음과 같다.

MemberController
    ↓
MemberService
    ↓
MemberRepository
    ↓
MemoryMemberRepository

즉, Controller는 Service에 의존하고, Service는 Repository 역할에 의존한다.

구체 구현체인 MemoryMemberRepository는 Spring Container가 연결해준다.


13. 체크 질문 QnA

Q1. Spring에서 IoC란 무엇인가?

IoC는 제어의 역전이라는 의미로, 개발자가 직접 객체를 생성하고 연결하던 제어권을 Spring Container에게 넘기는 것을 말한다.
Spring은 객체 생성, 의존성 연결, 생명주기 관리를 담당하고 개발자는 비즈니스 로직에 집중한다.

Q2. 객체를 직접 new로 생성하면 어떤 문제가 생기는가?

new로 직접 구현체를 생성하면 사용하는 클래스가 구체 구현체에 강하게 결합된다.
그 결과 구현체를 교체할 때 Service 코드까지 수정해야 하고, 테스트용 Fake 객체를 주입하기도 어려워진다.
즉, 변경에 약하고 테스트하기 어려운 구조가 된다.

Q3. DI란 무엇인가?

DI는 Dependency Injection의 약자로, 객체가 필요한 의존 객체를 직접 생성하지 않고 외부에서 주입받는 방식이다.
Spring에서는 Container가 Bean을 생성하는 과정에서 생성자 등을 통해 필요한 의존성을 주입해준다.
DI는 IoC를 구현하는 대표적인 방법이다.

Q4. Bean이란 무엇인가?

Bean은 Spring Container가 생성하고 관리하는 객체를 말한다.
Spring은 Bean의 생성, 의존성 주입, 생명주기 관리를 담당한다.

Q5. @Service, @Repository, @Controller는 각각 어떤 역할을 표현하는가?

@Service는 비즈니스 로직을 담당하는 클래스에 붙인다.
@Repository는 DB 접근, 데이터 저장, 조회 같은 영속성 계층 클래스에 붙인다.
@Controller 또는 @RestController는 HTTP 요청을 받아 URL을 매핑하고 Service를 호출하는 웹 계층 클래스에 붙인다.

Q6. 생성자 주입이 필드 주입보다 나은 이유는 무엇인가?

생성자 주입은 필요한 의존성이 없으면 객체를 생성할 수 없기 때문에 의존성 누락을 빠르게 발견할 수 있다.
또한 필드를 final로 선언할 수 있어 불변성을 유지할 수 있고, 테스트에서 Fake 객체를 직접 주입하기 쉽다.
클래스가 필요한 의존성도 생성자를 통해 명확히 드러난다.

Q7. 같은 타입의 Bean이 2개 이상이면 어떤 문제가 생기는가?

같은 타입의 Bean 후보가 여러 개 있고 추가 정보가 없으면 Spring은 어떤 Bean을 주입해야 할지 결정하지 못한다.
이때 @Primary로 기본 Bean을 지정하거나, @Qualifier로 특정 Bean 이름을 명시해서 해결할 수 있다.

Q8. Service가 구체 구현체가 아니라 역할에 의존해야 하는 이유는 무엇인가?

역할에 의존하면 Service는 특정 구현 방식에 덜 묶이게 된다.
따라서 MemoryMemberRepository에서 JdbcMemberRepository로 구현체가 바뀌어도 Service 코드를 수정하지 않아도 된다.
이렇게 역할과 구현을 분리하면 결합도가 낮아지고 변경에 강한 구조가 된다.

Q9. @SpringBootApplication은 컴포넌트 스캔과 어떤 관련이 있는가?

@SpringBootApplication은 Spring Boot 애플리케이션의 시작 설정을 나타내며, 자동 설정과 컴포넌트 스캔 기능을 포함한다.
Spring Boot는 @SpringBootApplication이 붙은 클래스의 패키지를 기준으로 하위 패키지를 스캔한다.
그 과정에서 @Component, @Service, @Repository, @Controller 등이 붙은 클래스를 Bean 후보로 탐색하고, Bean 생성 과정에서 필요한 의존성을 주입한다.

Q10. IoC, DI, Bean의 관계를 한 문장으로 설명하면?

Spring은 IoC 원칙에 따라 객체 생성과 관리를 Container가 담당하고, DI 방식으로 Bean들 사이의 의존성을 연결한다.
이때 Spring Container가 생성하고 관리하는 객체를 Bean이라고 한다.

14. 오늘의 한 문장 결론

Spring의 IoC, DI, Bean은 객체 생성과 의존관계 연결을 Spring Container에게 맡겨 변경에 강하고 테스트하기 쉬운 구조를 만들기 위한 핵심 개념이다.
반응형