ㄴㅏ랏말ㅆ.ㅁㅣ
article thumbnail

UserController와 스프링 컨테이너

  • UserController
  • 스프링 컨테이너

UserController

public class UserController {

    private final UserService userService;
    private final JdbcTemplate jdbcTemplate;

    public UserController(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
        this.userService = new UserService(jdbcTemplate);
    }
}

UserControllerJdbcTemplate에 의존하고 있다. 설정해준적 없는 JdbcTemplate@RestController 덕에 사용할 수 있다

@RestController 는 클래스를 API의 진입지점으로 만듦과 클래스를 스프링 빈으로 등록 시킨다


스프링 빈

서버가 시작되면, 스프링 서버 내부 거대한 컨테이너를 만듦

컨테이너 안에는 클래스가 들어가며 다양한 정보도 들어있고 인스턴스화도 이루어짐

-> 서로 필요한 관계에 있는 스프링 빈끼리 연결해 준다


우리가 가져온 의존성파일 안에 Jdbctemplate 등록되어 있음

서버 로딩과정

  1. 스프링 컨테이너(클래스 저장소) 시작
  2. 많은 스프링 빈(클래스)들이 등록됨(ex. jdbcTemplate, datesource)
  3. 설정해둔 스프링빈(클래스) 등록됨(ex. userController)
  4. 이때 필요한 의존성이 자동으로 설정됨

UserRepository가 JdbcTemplate을 바로 가져오지 못하는 이유는 스프링 빈이 아니어서 바로 가져오지 못함
@Repository란 어노테이션 붙이면 스프링 빈으로 등록됨


스프링 컨테이너

왜 사용할까?

인터페이스를 통한 최소한의 변경

But 이또한 오래거림 그래서 등장하게 된 스프링 컨테이너

-> 컨테이너가 BookService를 대신 인스턴스화 하고, 그 때 알아서 스프링 컨테이너를(BookRepository) 결정해준다 이러한 방식을 제어의 역전(Ioc, Inversion of Control) 이라 한다
(컨테이너가 의존성 선택하는 과정)

컨테이너가 선택해 BookService에 넣어주는 과정을 의존성 주입(DI, Dependency Injection)
(실제 의존성 넣어주는 과정)

@Repository가 2개 있다면 어떤 것을 쓸지 몰라 오류가 난다. 우선순위를 위해 @Primary를 붙여주면 우선순위를 갖게 된다


빈을 등록하는 방법

@configuratjion
- 클래스에 붙이는 어노테이션
- @Bean을 사용할 때 함께 사용해 주어야 한다

@Bean
- 메소드에 붙이는 어노테이션
- 메소드에서 반환되는 객체를 스프링 빈에 등록한다
@Configuration
public class UserConfiguration {

    @Bean
    public UserRepository userRepository(JdbcTemplate jdbcTemplate) {
        return new UserRepository(jdbcTemplate);
    }
}

보통 @Service, @Repository는 개발자가 직접 만든 클래스를 스프링 빈으로 등록할 때 사용

@Configuration + @Bean은 외부 라이브러리, 프레임워크에서 만든 클래스를 등록할 때 사용(Jdbc도 이렇게 만들어져 있음)

@Component
- 주어진 클래스를 '컴포넌트'로 간주
- 이 클래스들은 스프링 서버가 뜰 때 자동으로 감지됨 

1. 컨트롤러, 서비스, 리포지토리가 모두 아니고
2. 개발자가 직접 작성한 클래스를 스프링 빈으로 등록할 때 사용

스프링 빈을 주입 받는 몇 가지 방법

  1. (가장 권장) 생성자를 이용해 주입받는 방식

    @RestController
    // @Autowired
    public class BookController {
    
        private final BookService bookService;
    
        public BookController(BookService bookService) {
            this.bookService = bookService;
        }
    }
    원래 생성자 위에 `@Autowired` 라고 
    생성자에 있는 매개변수에 스프링 빈을 넣어주고, 자동으로 연결시켜 달라는 것이 있었지만
    스프링 버전이 업데이트 되면서 사용하지 않아도 되게됨 

    Spring 4.3부터 사용하지 않아도 되었지만, 명시적 의존성 주입 필요한 경우에는 여전히 사용해야함

    Spring 5.0 부터는 @Autowired의 개념을 보완한 @Inject을 사용하는데 @Autowired는 스프링의 어노테이션 이지만 @Inject는 자바 표준에 정의되어 있다, 또한 다른 DI와의 호환성에도 유용하다

    -> @Autowired 생략 가능


  1. setter@Autowired사용

    private UserService userService;
    
    @Autowired
    public void setUserService(UserService userService) {
         this.userService = userService;
    }
    setter에 @Autowired 적용

    -> 누군가 setter를 사용하면 오작동 할 수 있다


  1. 필드에 직접 @Autowired 사용

    @Autowired
    private UserService userService;

    -> 테스트를 어렵게 만듦


@Qualifier 란?

여러개의 후보군이 있을때 하나를 특정해서 가져올때 클래스의 이름(스프링 빈의 이름이 될)을 지정

등록할때 부터 @Qualifier("이름") 특정 이름으로 등록해서 가져올 수 있도록 함

private final UserService userService;
private final FruitService fruitService;

public UserController(UserService userService, @Qualifier("appleService") FruitService fruitService) {
    this.userService = userService;
    this.fruitService = fruitService;
}

스프링 빈을 사용하는 쪽, 스프링 빈을 등록하는 쪽 모두 @Qualifier 사용가능

스프링 빈을 사용하는 쪽에서만 쓸시, 빈의 이름을 적어줘야 함

양쪽 모두 사용하면, @Qualifier 끼리 연결됨


@Primary vs @Qualifier 누구를 가져올 것인가?

사용하는 쪽이 직접 적은 @Qualifier를 가져온다


문제

1. 과제 4의 내용 Controller, Service, Repository로 분리

Controller

@RequestMapping("/api/v1")
@RestController
public class StoreController {

    private FruitService fruitService;

    @PostMapping("/fruit")
    public void putFruit(@RequestBody Fruit request) {
        fruitService.createFruit(request);
    }

    @PutMapping("/fruit")
    public void checkFruit(@RequestBody Fruit request) {
        fruitService.checkFruit(request);
    }

    @GetMapping("/fruit/stat")
    public FruitSaleRequest saleFruit(@RequestParam String name) {
        return fruitService.saleFruit(name);
    }
}

Service

@Service
public class FruitService {

    private final FruitRepository fruitRepository;

    public FruitService(FruitRepository fruitRepository) {
        this.fruitRepository = fruitRepository;
    }

    public void createFruit(Fruit fruit) {
        fruitRepository.createFruit(fruit.getName(), fruit.getWarehousingDate(), fruit.getPrice());
    }

    public void checkFruit(Fruit fruit) {

        if (fruitRepository.isFruit(fruit.getId()))
            throw new IllegalArgumentException();

        fruitRepository.checkIdFruit(fruit.getId());
    }


    public FruitSaleRequest saleFruit(String name) {

        if (fruitRepository.checkNameFruit(name))
            throw new IllegalArgumentException();

        long salesAmount = fruitRepository.saleFruit("salesAmount", true);
        long notSalesAmount = fruitRepository.saleFruit("notSalesAmount", false);

        return new FruitSaleRequest(salesAmount, notSalesAmount);
    }
}
@Repository
public class FruitRepository {

    private final JdbcTemplate jdbcTemplate;

    public FruitRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    public void createFruit(String name, LocalDate date, long price){
        String sql = "INSERT INTO fruit (name, warehousingDate, price) values (?, ?, ?)";

        jdbcTemplate.update(sql, name, date, price);
    }

    public boolean isFruit(long id) {
        String checkSql = "SELECT * from fruit where id=?";
        return jdbcTemplate.query(checkSql, (rs, rowNum) -> 0, id).isEmpty();
    }

    public void checkIdFruit(long id) {
        String sql = "UPDATE fruit SET sold = 1 WHERE id = ?";

        jdbcTemplate.update(sql, id);
    }

    public boolean checkNameFruit(String name) {
        String readSql = "SELECT 8 from fruit WHERE name = ?";
        return jdbcTemplate.query(readSql, (rs, rowNum) -> 0, name).isEmpty();
    }

    public long saleFruit(String name, boolean sold) {
        String sql = "SELECT sum(price) as ? FROM fruit WHERE sold = ?";

        return jdbcTemplate.queryForObject(sql, new Object[]{name, sold}, (rs, rowNum) -> rs.getLong(name));
    }
}

2. 기존의 FruitRepositoryFruitMemoryRepositoryFruitMySqlRepository로 나누고 @Primary 어노테이션 활용해 두 Repository를 바꿔가며 동작하게 하기 (@Qualifier 사용해도 됨)

interface

public interface FruitRepositoryV2 {

  void saveFruit(FruitCreateRequest request);

  boolean isFruitForSale(long id);

  void updateFruitName(long id);

  FruitSaleResponse getTotalPrice(String name);
}

Memory

@Repository
public class FruitMemoryRepository implements FruitRepositoryV2 {

  private final List<Fruit> fruits = new ArrayList<>();

  @Override
  public void saveFruit(FruitCreateRequest request) {
    fruits.add(new Fruit(request.getName(), request.getWarehousingDate(), request.getPrice()));
    System.out.println(request.toString());
  }

  @Override
  public boolean isFruitForSale(long id) {
    return false;
  }

  @Override
  public void updateFruitName(long id) {
    for (Fruit fruit : fruits) {
      if (fruit.getId() == id) {
        updateFruitName(id);
      }
      System.out.println(id);
    }
  }

  @Override
  public FruitSaleResponse getTotalPrice(String name) {
    long salesAmount = 0;
    long notSalesAmount = 0;

    for (Fruit fruit : fruits) {
      if (fruit.getName().equals(name)) {
        if (fruit.getIs_sale()) {
          salesAmount += fruit.getPrice();
        } else {
          notSalesAmount += fruit.getPrice();
        }
      }
    }
    return new FruitSaleResponse(salesAmount, notSalesAmount);
  }
}

Repository

@Repository
@Primary
public class FruitMySqlRepository implements FruitRepositoryV2{

  private final JdbcTemplate jdbcTemplate;

  public FruitMySqlRepository(JdbcTemplate jdbcTemplate) {
    this.jdbcTemplate = jdbcTemplate;
  }

  @Override
  public void saveFruit(FruitCreateRequest request) {
    String sql = "INSERT INTO fruit (name, warehousing_date, price) VALUES (?, ?, ?)";
    jdbcTemplate.update(sql, request.getName(), request.getWarehousingDate(), request.getPrice());
  }

  @Override
  public boolean isFruitForSale(long id) {
    String Salesql = "SELECT * FROM fruit WHERE id = ?";
    return jdbcTemplate.query(Salesql, (rs, rowNum) -> 0, id).isEmpty();
  }

  @Override
  public void updateFruitName(long id) {
    String sql = "UPDATE fruit SET is_sale = 1 WHERE id = ?";
    jdbcTemplate.update(sql, id);
  }

  @Override
  public FruitSaleResponse getTotalPrice(String name) {
    String sql = "SELECT"+
            "(SELECT SUM(price) FROM fruit WHERE is_sale = 1 AND name = ?) AS salesAmount, "+
            "(SELECT SUM(price) FROM fruit WHERE is_sale = 0 AND name = ?) AS notSalesAmount";
    return jdbcTemplate.queryForObject(sql, new Object[]{name, name}, (rs, rowNum) -> {
      long salesAmount = rs.getLong("salesAmount");
      long notSalesAmount = rs.getLong("notSalesAmount");
      return new FruitSaleResponse(salesAmount, notSalesAmount);
    });
  }

}

출처

profile

ㄴㅏ랏말ㅆ.ㅁㅣ

@동여우

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!

검색 태그