[Spring/SpringBoot] 03. 스프링/스프링부트 DI(Dependency Injection)

거북이·2024년 4월 1일
0

Spring/SpringBoot/JPA

목록 보기
3/6
post-thumbnail

📌Dependency(의존성)

A가 B를 의존한다?

A가 B를 의존한다는 말은 B의 변화가 감지된다면 A에게까지 파급 효과가 미친다는 것을 말한다.

요리사와 레시피의 관계를 예시로 들 수 있다.

요리사는 레시피를 참고하여 요리를 만들 수 있다. 만약 레시피가 변화되었다면 요리사는 변화된 레시피에 따라 요리를 만들 수 있게 된다. 레시피의 변화가 요리사의 요리에 영향을 미쳤기 때문에 요리사는 레시피에 의존한다.라고 설명할 수 있게 되는 것이다.

package di;

public class Chef {
    private Recipe recipe;

    public Chef() {
        recipe = new Recipe();
    }
}

위와 같이 클래스에 직접적으로 의존하게 되면 결합도가 높아져 이렇게 되면 유지보수가 어려워진다는 단점이 생긴다. 레시피는 여러 가지가 존재할 수 있는데 그 때마다 레시피를 바꾸면 많은 수정을 필요로 하게 된다.

📌의존관계를 인터페이스로 추상화하기

또 다른 예시로 핸드폰을 들어보자. 우리가 실생활에서 갤럭시, 아이폰, LG 등등 여러 회사의 제품을 사용하고 있다. 핸드폰을 교체할 때마다 사용자가 사용하는 핸드폰의 기종을 그 때마다 교체한다면 코드의 수정은 불가피하다. 갤럭시, 아이폰, LG 등등의 핸드폰 제품의 공통점을 추출해 상위 타입으로 보았을 때 상위 타입은 바로 핸드폰이 되는 것이다. 특정 핸드폰에 의존하는 것이 아닌 핸드폰이라는 상위 타입의 공통점에 의존하는 것이다.

다시 위의 예시로 돌아와서 현재 코드를 보면 요리사는 Recipe클래스에만 의존하고 있는 것을 볼 수 있다. 요리사가 다양한 레시피에 의존할 수 있도록 하려면 코드를 아래와 같이 수정하면 된다.

package di;

public class Chef {
    private FoodRecipe foodRecipe;

    public Chef() {
        foodRecipe = new FoodA();
        // foodRecipe = new FoodB();
    }
}

레시피의 변화에 따라서 전체 코드를 수정할 필요없이 다형적 참조를 활용해 결합도를 낮추고 유지보수의 용이성을 높일 수 있다.

📌Dependency Injection

의존관계 주입을 수행하는 방법은 크게 2가지가 있다.

[1]. 어노테이션을 이용한 컴포넌트 스캔과 자동 의존관계 주입 방법
[2]. 자바 코드 설정 파일을 작성해 직접 스프링 빈으로 등록하는 방법

Ex1. MemberRepository인터페이스를 구현한 구현체 클래스 MemoryMemberRepository를 사용 & 어노테이션을 이용한 컴포넌트 스캔과 자동 의존관계 주입 방법

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

    private final MemberService memberService;

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

    private final MemberRepository memberRepository;

    @Autowired
    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
    
    // ...
}
@Repository
public class MemoryMemberRepository implements MemberRepository{
	// ...
}
  • @Component 어노테이션이 있으면 스프링 빈으로 자동 등록이 된다.
  • 그런데 위의 코드를 보면 @Controller, @Service, @Repository 어노테이션만 있을 뿐 @Component 어노테이션은 붙어 있지 않았는데 어떻게 스프링 빈으로 등록이 되었을까...
  • 아래 각 어노테이션의 코드를 보면 공통적으로 @Component 어노테이션이 있는 것을 볼 수 있는데 이 때문에 스프링 빈으로 등록이 된다고 볼 수 있다.

Ref1. @Controller 코드

// ...

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component	// 확인
public @interface Controller {
    @AliasFor(
        annotation = Component.class
    )
    String value() default "";
}

Ref2. @Service 코드

// ...

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component	// 확인
public @interface Service {
    @AliasFor(
        annotation = Component.class
    )
    String value() default "";
}

Ref3. @Repository 코드

// ...

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component	// 확인
public @interface Repository {
    @AliasFor(
        annotation = Component.class
    )
    String value() default "";
}

Ex2. MemberRepository인터페이스를 구현한 구현체 클래스 MemoryMemberRepository를 사용 & 자바 코드로 설정 파일을 작성하여 직접 스프링 빈으로 등록하는 방법

❗앞서 어노테이션을 이용한 컴포넌트 스캔 때 작성했던 @Service, @Repository, @Autowired 어노테이션을 제거(단, @Controller는 남겨둠)

package hello.springboot;

import hello.springboot.repository.MemberRepository;
import hello.springboot.repository.MemoryMemberRepository;
import hello.springboot.service.MemberService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SpringConfig {

    @Bean
    public MemberService memberService() {
        return new MemberService(memberRepository());
    }

    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
}

스프링 컨테이너는 @Configuration 어노테이션이 붙은 클래스를 자동으로 빈으로 등록해두고 해당 클래스를 파싱해서 @Bean 어노테이션이 붙은 메서드를 전부 찾아서 생성된 객체로 빈을 등록한다.


Ex3. 스프링 JPA를 활용 & 어노테이션을 이용한 컴포넌트 스캔과 자동 의존관계 주입 방법

Ex1과 같이 다시 어노테이션을 붙여주고 스프링 JPA 인터페이스를 작성

package hello.springboot.repository;

import hello.springboot.domain.Member;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {
    Optional<Member> findByName(String name);
}

JPA의 경우 JpaRepository 인터페이스를 상속받아 사용한다.

❗인터페이스를 상속(extends)?, 구현(implements)?

인터페이스를 구현한 구현체 클래스의 경우에는 implements를 사용하는 것이 맞고 인터페이스가 인터페이스를 상속받는 경우에는 extends를 사용한다.
또한 인터페이스는 다중 상속을 지원하므로 JpaRepositoryMemberRepository를 상속받는다.

대개 인터페이스를 사용면 인터페이스의 추상 메서드를 구현한 구현체 클래스가 존재해야하는데 그 구현체가 없다. 하지만 JpaRepository를 상속받고 있으면 구현체 클래스를 자동으로 만들고 스프링 빈으로 등록해준다.(개발자가 등록하는 것이 아니다.)


Ex4. 스프링 JPA를 활용 & 자바 코드로 설정 파일을 작성하여 직접 스프링 빈으로 등록하는 방법★

이 과정을 공부하면서 아래와 같은 에러가 계속 발생했었다.

Parameter 0 of constructor in hello.springboot.service.MemberService required a single bean, but 2 were found:

이 에러는 스프링 빈이 2개 이상 등록되어 발생하는 에러다. @Repository 어노테이션이 붙은 부분도 제거를 했고 자바 코드로 작성한 설정 파일인 SpringConfig에서 @Configuration@Bean 어노테이션을 붙여 직접 스프링 빈으로 등록하도록 했는데 스프링 빈이 2개 이상 등록이 되었다는 문제가 계속 발생해 매우 난감했다.

MemberRepository 인터페이스를 구현한 메모리 Repository 구현체 클래스의 이름을 처음에 MemberRepositoryImpl로 작성해서 했다. 근데 이것이 문제가 되었던 것이다.

사용자 정의 구현 클래스 규칙에 해당하여 뜻하지 않게 스프링 빈으로 등록이 되어버린 것이다.

사용자 정의 구현 클래스

  • 규칙 : 리포지토리 인터페이스 이름 + Impl
    위와 같은 규칙으로 이름을 붙이면 스프링 데이터 JPA가 인식해서 스프링 빈으로 등록

그래서 이 부분을 감안하여 MemberRepository 인터페이스를 구현한 구현체 클래스 이름을 다른 이름으로 바꾸었더니 정상적으로 동작하는 것을 확인할 수 있었다.

스프링 JPA를 사용할 때 자바 코드로 직접 설정 파일을 작성할 때 스프링 JPA를 개발자가 직접 스프링 빈으로 등록하는 것이 아니다. 따라서 이 부분을 고려하여 SpringConfig 파일을 작성한다.

package hello.springboot;

import hello.springboot.repository.MemberRepository;
import hello.springboot.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SpringConfig {

    private final MemberRepository memberRepository;

    @Autowired
    public SpringConfig(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    @Bean
    public MemberService memberService() {
        return new MemberService(memberRepository);
    }
}

📌생성자를 이용해서 의존관계 주입(권장되는 방식)

SpringConfig 파일에서 생성자를 통한 의존관계 주입이 이루어지는 것을 볼 수 있다.

@Configuration
public class SpringConfig {

    private final MemberRepository memberRepository;

    @Autowired
    public SpringConfig(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
	// ...
}

❗private 접근 제어자 : 외부로부터의 접근을 제어

❗final 키워드 : 재할당을 불가능하게함으로써 객체의 불변을 보장할 수 있다.

이 때, 생성자에 @Autowired 어노테이션이 있는 것을 볼 수 있는데 이 @Autowired 어노테이션은 생성자가 만약 하나만 존재한다면 생략할 수 있다.

JDBC나 JdbcTemplate, JPA 등은 스프링을 사용하는 환경이므로 @Repository를 통한 스프링이 관리하는 객체를 등록(생성)하고 생성자 주입(@Autowired MemberRepository memberRepository) 혹은 필드 주입으로 의존관계를 주입하여 사용한다.

📌출처

인프런 - 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술
인프런 커뮤니티 - Parameter 0 of constructor in hello.springboot.service.MemberService required a single bean, but 2 were found:
https://mangkyu.tistory.com/125

0개의 댓글