블로그

양성빈

[인프런 워밍업 스터디 클럽] 0기 백엔드 미션 - API 레이어 분리 테스트 (Day6)

과제진도표 6일차와 연결됩니다우리는 스프링 컨테이너의 개념을 배우고, 기존에 작성했던 Controller 코드를 3단 분리해보았습니다. 앞으로 API를 개발할 때는 이 계층에 맞게 각 코드가 작성되어야 합니다! 🙂과제 #4 에서 만들었던 API를 분리해보며, Controller - Service - Repository 계층에 익숙해져 봅시다! 👍문제 1과제4에서 만들었던 API를 Controller - Service - Repository로 분리하라고 하셨다.하지만 이전에 과제4를 진행하면서 나는 이미 레이어를 분리했지만 강의에 대한 복습 겸, 다시 진행해보기로 했다.step0. DB 생성 및 테이블 생성먼저 데이터베이스부터 다시 만들기로 하였다. 아래와 같이 쿼리를 작성하여 데이터베이스를 생성한다.create database fruit;다음으로 내가 생성한 fruit 데이터베이스에 접속한다.use fruit;그리고 테이블 목록을 조회해본다. 당연히 비어 있을 것이다.show tables;그러면 아래와 같이 테이블 목록들이 비어있는 것을 확인할 수 있을 것이다.그러면 이제 아래와 같이 쿼리를 작성해서 테이블을 만들어보자. 테이블 컬럼들은 기존과 동일하게 적용한다.CREATE TABLE fruit ( id bigint auto_increment, name varchar(20) not null, warehousingDate date not null, price bigint not null, is_sold boolean not null default false, primary key (id) );그리고 테이블이 잘 생성 되었는지 조회를 해서 확인해본다.show tables;step1. DB 설정 정보 적용이제 DB 연결정보를 Spring Boot 프로젝트와 연결해보자.프로젝트의 src/main/resources의 경로에 있는 application.properties를 application.yml로 변경하고 설정정보를 아래와 같이 작성한다.spring: datasource: url: "jdbc:mysql://localhost/fruit" username: "root" password: "" driver-class-name: com.mysql.cj.jdbc.Driver⚠ 주의username과 password는 본인에 따라 달리 작성한다.또한 굳이 application.yml 로 확장자 변경을 안하고 properties 확장자로 이용해도 무관하다.step2. 기존 컨트롤러 클래스 파일 가져오기나는 이미 과제4에서 레이어를 분리해두었다. 하지만 이번 과제의 취지에 맞게 기존에 파일들을 가져오기는 하지만 비즈니스 로직들을 컨트롤럴 클래스에 포함된 파일들로 가져오기로 하였다.Fruit.javapackage me.sungbin.entity.fruit; import java.time.LocalDate; /** * @author : rovert * @packageName : me.sungbin.controller.fruit * @fileName : Fruit * @date : 2/25/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/25/24 rovert 최초 생성 */ public class Fruit { private long id; private String name; private LocalDate warehousingDate; private long price; private boolean isSold; public Fruit() { } public Fruit(String name, LocalDate warehousingDate, long price) { this.name = name; this.warehousingDate = warehousingDate; this.price = price; } public Fruit(String name, LocalDate warehousingDate, long price, boolean isSold) { this.name = name; this.warehousingDate = warehousingDate; this.price = price; this.isSold = isSold; } public long getId() { return id; } public String getName() { return name; } public LocalDate getWarehousingDate() { return warehousingDate; } public long getPrice() { return price; } public boolean isSold() { return isSold; } }SaveFruitInfoRequestInfo.javapackage me.sungbin.dto.fruit.request; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import me.sungbin.entity.fruit.Fruit; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDate; /** * @author : rovert * @packageName : me.sungbin.dto.fruit.request * @fileName : SaveFruitInfoRequestDto * @date : 2/25/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/25/24 rovert 최초 생성 */ public class SaveFruitInfoRequestDto { @NotBlank(message = "과일 이름이 공란일 수 없습니다.") @NotNull(message = "과일 이름이 null일 수는 없습니다.") private String name; @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) private LocalDate warehousingDate; @Min(value = 0, message = "가격이 음수가 될 수는 없습니다.") private long price; public SaveFruitInfoRequestDto(String name, LocalDate warehousingDate, long price) { this.name = name; this.warehousingDate = warehousingDate; this.price = price; } public String getName() { return name; } public LocalDate getWarehousingDate() { return warehousingDate; } public long getPrice() { return price; } public Fruit toEntity() { return new Fruit(name, warehousingDate, price); } }UpdateFruitRequestDto.javapackage me.sungbin.dto.fruit.request; /** * @author : rovert * @packageName : me.sungbin.dto.fruit.request * @fileName : UpdateFruitRequestDto * @date : 2/25/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/25/24 rovert 최초 생성 */ public class UpdateFruitRequestDto { private long id; public UpdateFruitRequestDto() { } public UpdateFruitRequestDto(long id) { this.id = id; } public long getId() { return id; } }GetFruitResponseDto.javapackage me.sungbin.dto.fruit.response; /** * @author : rovert * @packageName : me.sungbin.dto.fruit.response * @fileName : GetFruitResponseDto * @date : 2/25/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/25/24 rovert 최초 생성 */ public class GetFruitResponseDto { private long salesAmount; private long notSalesAmount; public GetFruitResponseDto(long salesAmount, long notSalesAmount) { this.salesAmount = salesAmount; this.notSalesAmount = notSalesAmount; } public long getSalesAmount() { return salesAmount; } public long getNotSalesAmount() { return notSalesAmount; } }FruitController.javapackage me.sungbin.controller.fruit; import jakarta.validation.Valid; import me.sungbin.dto.fruit.request.SaveFruitInfoRequestDto; import me.sungbin.dto.fruit.response.GetFruitResponseDto; import me.sungbin.entity.fruit.Fruit; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.web.bind.annotation.*; import java.util.HashMap; import java.util.Map; /** * @author : rovert * @packageName : me.sungbin.controller.fruit * @fileName : FruitController * @date : 2/25/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/25/24 rovert 최초 생성 */ @RestController @RequestMapping("/api/v1/fruit") public class FruitController { private final JdbcTemplate jdbcTemplate; public FruitController(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } @PostMapping public void saveFruitInfo(@RequestBody @Valid Fruit fruit) { String sql = "INSERT INTO fruit (name, warehousingDate, price) VALUES (?, ?, ?)"; this.jdbcTemplate.update(sql, fruit.getName(), fruit.getWarehousingDate(), fruit.getPrice()); } @PutMapping public void updateFruitInfo(@RequestBody Fruit fruit) { String readSQL = "SELECT * FROM fruit WHERE id = ?"; boolean isNotExistsFruitInfo = jdbcTemplate.query(readSQL, (rs, rowNum) -> 0, fruit.getId()).isEmpty(); if (isNotExistsFruitInfo) { throw new IllegalArgumentException("존재하는 과일정보가 없습니다."); } String sql = "UPDATE fruit SET is_sold = 1 WHERE id = ?"; jdbcTemplate.update(sql, fruit.getId()); } @GetMapping("/stat") public GetFruitResponseDto getFruitInfo(@RequestParam String name) { long salesAmount = 0; long notSalesAmount = 0; String sql = "SELECT SUM(price) as total, is_sold FROM fruit WHERE name = ? GROUP BY is_sold"; Map<Boolean, Long> aggregatedData = jdbcTemplate.query(sql, new Object[]{name}, rs -> { HashMap<Boolean, Long> map = new HashMap<>(); while (rs.next()) { map.put(rs.getBoolean("is_sold"), rs.getLong("total")); } return map; }); if (aggregatedData.containsKey(true)) { salesAmount = aggregatedData.get(true); } if (aggregatedData.containsKey(false)) { notSalesAmount = aggregatedData.get(false); } if (salesAmount == 0L && notSalesAmount == 0L) { throw new IllegalArgumentException("존재하는 과일이 없습니다."); } return new GetFruitResponseDto(salesAmount, notSalesAmount); } }step3. 레이어 분리이제 레이어를 분리해보겠다. 일단 현재 컨트롤러에는 HTTP 통신하는 부분과 DB처리 관련 로직, 예외로직이 엄청 많다. 이것은 클린코드의 단일책임원칙에 위배가 되므로 서비스 레이어를 만들어 분리해보도록 하자.FruitService.javapackage me.sungbin.service.fruit; import me.sungbin.dto.fruit.response.GetFruitResponseDto; import me.sungbin.entity.fruit.Fruit; import org.springframework.jdbc.core.JdbcTemplate; import java.util.HashMap; import java.util.Map; /** * @author : rovert * @packageName : me.sungbin.service.fruit * @fileName : FruitService * @date : 2/25/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/25/24 rovert 최초 생성 */ public class FruitService { private final JdbcTemplate jdbcTemplate; public FruitService(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } public void saveFruitInfo(Fruit fruit) { String sql = "INSERT INTO fruit (name, warehousingDate, price) VALUES (?, ?, ?)"; this.jdbcTemplate.update(sql, fruit.getName(), fruit.getWarehousingDate(), fruit.getPrice()); } public void updateFruitInfo(Fruit fruit) { String readSQL = "SELECT * FROM fruit WHERE id = ?"; boolean isNotExistsFruitInfo = jdbcTemplate.query(readSQL, (rs, rowNum) -> 0, fruit.getId()).isEmpty(); if (isNotExistsFruitInfo) { throw new IllegalArgumentException("존재하는 과일정보가 없습니다."); } String sql = "UPDATE fruit SET is_sold = 1 WHERE id = ?"; jdbcTemplate.update(sql, fruit.getId()); } public GetFruitResponseDto getFruitInfo(String name) { long salesAmount = 0; long notSalesAmount = 0; String sql = "SELECT SUM(price) as total, is_sold FROM fruit WHERE name = ? GROUP BY is_sold"; Map<Boolean, Long> aggregatedData = jdbcTemplate.query(sql, new Object[]{name}, rs -> { HashMap<Boolean, Long> map = new HashMap<>(); while (rs.next()) { map.put(rs.getBoolean("is_sold"), rs.getLong("total")); } return map; }); if (aggregatedData.containsKey(true)) { salesAmount = aggregatedData.get(true); } if (aggregatedData.containsKey(false)) { notSalesAmount = aggregatedData.get(false); } if (salesAmount == 0L && notSalesAmount == 0L) { throw new IllegalArgumentException("존재하는 과일이 없습니다."); } return new GetFruitResponseDto(salesAmount, notSalesAmount); } }FruitController.javapackage me.sungbin.controller.fruit; import jakarta.validation.Valid; import me.sungbin.dto.fruit.request.SaveFruitInfoRequestDto; import me.sungbin.dto.fruit.response.GetFruitResponseDto; import me.sungbin.entity.fruit.Fruit; import me.sungbin.service.fruit.FruitService; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.web.bind.annotation.*; import java.util.HashMap; import java.util.Map; /** * @author : rovert * @packageName : me.sungbin.controller.fruit * @fileName : FruitController * @date : 2/25/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/25/24 rovert 최초 생성 */ @RestController @RequestMapping("/api/v1/fruit") public class FruitController { private final FruitService fruitService; public FruitController(JdbcTemplate jdbcTemplate) { this.fruitService = new FruitService(jdbcTemplate); } @PostMapping public void saveFruitInfo(@RequestBody @Valid Fruit fruit) { this.fruitService.saveFruitInfo(fruit); } @PutMapping public void updateFruitInfo(@RequestBody Fruit fruit) { this.fruitService.updateFruitInfo(fruit); } @GetMapping("/stat") public GetFruitResponseDto getFruitInfo(@RequestParam String name) { return this.fruitService.getFruitInfo(name); } }좀 더 컨트롤러 클래스가 깔끔해진 것을 볼 수 있다. 하지만 서비스 클래스에 DB 관련 처리과 더해 예외로직들이 있는 것은 클린코드에 위배되는 것 같다. 따라서 FruitService 코드도 레파지토리 레이어를 만들어서 분리해보도록 하자. 그리고 각각 리팩토링 작업도 거쳤다. 아래의 코드를 보자. FruitService.javapackage me.sungbin.service.fruit; import me.sungbin.dto.fruit.response.GetFruitResponseDto; import me.sungbin.entity.fruit.Fruit; import me.sungbin.repository.fruit.FruitRepository; import org.springframework.jdbc.core.JdbcTemplate; /** * @author : rovert * @packageName : me.sungbin.service.fruit * @fileName : FruitService * @date : 2/25/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/25/24 rovert 최초 생성 */ public class FruitService { private final FruitRepository fruitRepository; public FruitService(JdbcTemplate jdbcTemplate) { this.fruitRepository = new FruitRepository(jdbcTemplate); } public void saveFruitInfo(Fruit fruit) { this.fruitRepository.saveFruitInfo(fruit); } public void updateFruitInfo(Fruit fruit) { validate(fruit.getId()); this.fruitRepository.updateFruitInfo(fruit); } public GetFruitResponseDto getFruitInfo(String name) { GetFruitResponseDto fruitData = this.fruitRepository.getFruitInfo(name); validateGetFruitAmount(fruitData.getSalesAmount(), fruitData.getNotSalesAmount()); return fruitData; } private void validate(long id) { if (this.fruitRepository.isNotExistsFruitInfo(id)) { throw new IllegalArgumentException("존재하는 과일정보가 없습니다."); } } private void validateGetFruitAmount(long salesAmount, long notSalesAmount) { if (salesAmount == 0L && notSalesAmount == 0L) { throw new IllegalArgumentException("존재하는 과일이 없습니다."); } } }FruitRepository.javapackage me.sungbin.repository.fruit; import me.sungbin.dto.fruit.response.GetFruitResponseDto; import me.sungbin.entity.fruit.Fruit; import org.springframework.jdbc.core.JdbcTemplate; import java.util.HashMap; import java.util.Map; import java.util.Objects; /** * @author : rovert * @packageName : me.sungbin.repository.fruit * @fileName : FruitRepository * @date : 2/25/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/25/24 rovert 최초 생성 */ public class FruitRepository { private final JdbcTemplate jdbcTemplate; public FruitRepository(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } public void saveFruitInfo(Fruit fruit) { String sql = "INSERT INTO fruit (name, warehousingDate, price) VALUES (?, ?, ?)"; this.jdbcTemplate.update(sql, fruit.getName(), fruit.getWarehousingDate(), fruit.getPrice()); } public void updateFruitInfo(Fruit fruit) { String sql = "UPDATE fruit SET is_sold = 1 WHERE id = ?"; jdbcTemplate.update(sql, fruit.getId()); } public GetFruitResponseDto getFruitInfo(String name) { String sql = "SELECT is_sold, SUM(price) AS total FROM fruit WHERE name = ? GROUP BY is_sold"; Map<Boolean, Long> aggregatedData = jdbcTemplate.query(sql, new Object[]{name}, rs -> { HashMap<Boolean, Long> map = new HashMap<>(); while (rs.next()) { map.put(rs.getBoolean("is_sold"), rs.getLong("total")); } return map; }); long salesAmount = Objects.requireNonNull(aggregatedData).getOrDefault(true, 0L); long notSalesAmount = Objects.requireNonNull(aggregatedData).getOrDefault(false, 0L); return new GetFruitResponseDto(salesAmount, notSalesAmount); } public boolean isNotExistsFruitInfo(long id) { String readSQL = "SELECT * FROM fruit WHERE id = ?"; return jdbcTemplate.query(readSQL, (rs, rowNum) -> 0, id).isEmpty(); } }하지만 뭔가 이상한 점을 발견할 수 있다. 현재 DB를 이용하는 것은 레파지토리 레이어이다. 즉, JdbcTemplate을 이용하는 것은 레파지토리 레이어뿐인 것이다. 하지만 코드를 보면 알 수 있듯이 컨트롤러, 서비스 레이어에도 전부 JdbcTemplate을 매개변수로 넣고 있다. 이런 것을 어떻게 해결할까? 바로 서비스와 레파지토리 레이어에 빈을 주입할 수 있는 어노테이션을 붙여준다. 이 부분은 오늘 강의시간에도 다뤘으니 적용해보자. FruitController.javapackage me.sungbin.controller.fruit; import jakarta.validation.Valid; import me.sungbin.dto.fruit.response.GetFruitResponseDto; import me.sungbin.entity.fruit.Fruit; import me.sungbin.service.fruit.FruitService; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.web.bind.annotation.*; /** * @author : rovert * @packageName : me.sungbin.controller.fruit * @fileName : FruitController * @date : 2/25/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/25/24 rovert 최초 생성 */ @RestController @RequestMapping("/api/v1/fruit") public class FruitController { private final FruitService fruitService; public FruitController(FruitService fruitService) { this.fruitService = fruitService; } @PostMapping public void saveFruitInfo(@RequestBody @Valid Fruit fruit) { this.fruitService.saveFruitInfo(fruit); } @PutMapping public void updateFruitInfo(@RequestBody Fruit fruit) { this.fruitService.updateFruitInfo(fruit); } @GetMapping("/stat") public GetFruitResponseDto getFruitInfo(@RequestParam String name) { return this.fruitService.getFruitInfo(name); } }FruitService.javapackage me.sungbin.service.fruit; import me.sungbin.dto.fruit.response.GetFruitResponseDto; import me.sungbin.entity.fruit.Fruit; import me.sungbin.repository.fruit.FruitRepository; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; /** * @author : rovert * @packageName : me.sungbin.service.fruit * @fileName : FruitService * @date : 2/25/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/25/24 rovert 최초 생성 */ @Service public class FruitService { private final FruitRepository fruitRepository; public FruitService(FruitRepository fruitRepository) { this.fruitRepository = fruitRepository; } public void saveFruitInfo(Fruit fruit) { this.fruitRepository.saveFruitInfo(fruit); } public void updateFruitInfo(Fruit fruit) { validate(fruit.getId()); this.fruitRepository.updateFruitInfo(fruit); } public GetFruitResponseDto getFruitInfo(String name) { GetFruitResponseDto fruitData = this.fruitRepository.getFruitInfo(name); validateGetFruitAmount(fruitData.getSalesAmount(), fruitData.getNotSalesAmount()); return fruitData; } private void validate(long id) { if (this.fruitRepository.isNotExistsFruitInfo(id)) { throw new IllegalArgumentException("존재하는 과일정보가 없습니다."); } } private void validateGetFruitAmount(long salesAmount, long notSalesAmount) { if (salesAmount == 0L && notSalesAmount == 0L) { throw new IllegalArgumentException("존재하는 과일이 없습니다."); } } } FruitRepository.javapackage me.sungbin.repository.fruit; import me.sungbin.dto.fruit.response.GetFruitResponseDto; import me.sungbin.entity.fruit.Fruit; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; import java.util.HashMap; import java.util.Map; import java.util.Objects; /** * @author : rovert * @packageName : me.sungbin.repository.fruit * @fileName : FruitRepository * @date : 2/25/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/25/24 rovert 최초 생성 */ @Repository public class FruitRepository { private final JdbcTemplate jdbcTemplate; public FruitRepository(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } public void saveFruitInfo(Fruit fruit) { String sql = "INSERT INTO fruit (name, warehousingDate, price) VALUES (?, ?, ?)"; this.jdbcTemplate.update(sql, fruit.getName(), fruit.getWarehousingDate(), fruit.getPrice()); } public void updateFruitInfo(Fruit fruit) { String sql = "UPDATE fruit SET is_sold = 1 WHERE id = ?"; jdbcTemplate.update(sql, fruit.getId()); } public GetFruitResponseDto getFruitInfo(String name) { String sql = "SELECT is_sold, SUM(price) AS total FROM fruit WHERE name = ? GROUP BY is_sold"; Map<Boolean, Long> aggregatedData = jdbcTemplate.query(sql, new Object[]{name}, rs -> { HashMap<Boolean, Long> map = new HashMap<>(); while (rs.next()) { map.put(rs.getBoolean("is_sold"), rs.getLong("total")); } return map; }); long salesAmount = Objects.requireNonNull(aggregatedData).getOrDefault(true, 0L); long notSalesAmount = Objects.requireNonNull(aggregatedData).getOrDefault(false, 0L); return new GetFruitResponseDto(salesAmount, notSalesAmount); } public boolean isNotExistsFruitInfo(long id) { String readSQL = "SELECT * FROM fruit WHERE id = ?"; return jdbcTemplate.query(readSQL, (rs, rowNum) -> 0, id).isEmpty(); } }각각 레파지토리 레이어와 서비스 레이어에 @Repository, @Service 어노테이션을 붙여주었고 이 어노테이션들은 @Component 어노테이션들을 붙어 있어서 빈을 주입 받을 수 있다. 그래서 각각 생성자 주입 방식으로 주입을 받았다.step4. 엔티티 대신에 DTO로!검색을 해보면 request나 response로 받아주는 것을 DTO로 받는게 좋다고 했다. 그이유는 아래와 같다. 📚 엔티티 대신에 DTO를 사용하는 이유?DTO(Data Transfer Object)를 엔티티 대신 사용하는 이유는 여러 가지가 있다. 첫째, DTO를 사용하면 애플리케이션의 프레젠테이션 계층과 데이터 접근 계층 사이의 의존성을 줄일 수 있어, 애플리케이션의 확장성과 유지보수성이 향상된다. 각 계층이 서로에 대해 덜 알고 있기 때문에, 변경 사항이 한 계층에만 국한되어 다른 계층에는 영향을 주지 않는 경우가 많다.둘째, DTO를 사용하면 클라이언트에 전송되는 데이터의 양과 형식을 조정할 수 있어, 네트워크를 통한 데이터 전송량을 최적화하고, 클라이언트가 필요로 하는 데이터 형식을 맞춤 제공할 수 있다. 이는 특히 모바일 애플리케이션 개발이나 대역폭이 제한된 환경에서 중요하다.셋째, DTO를 사용하면 엔티티의 모든 정보를 클라이언트에 노출하지 않아도 된다. 이는 보안 측면에서 매우 중요한데, 예를 들어 사용자 엔티티에는 비밀번호와 같은 민감한 정보가 포함될 수 있으나, 이를 DTO를 통해 필터링하고 클라이언트에 필요한 정보만 전달할 수 있다.넷째, 엔티티의 경우 JPA와 같은 ORM 기술을 사용할 때 지연 로딩(Lazy Loading) 등의 문제로 인해 직렬화에 어려움이 있을 수 있습니다. DTO를 사용하면 이러한 문제를 피하고, 데이터 전송을 위해 최적화된 객체를 생성할 수 있습니다.이러한 이유로 한번 DTO로 변경해보자. 현재 DTO는 과제4에서 사용했던 DTO를 이용할 것이다. 그리고 이 DTO의 코드내용은 step2에서 보여줬으므로 이것을 이용해보자.FruitController.javapackage me.sungbin.controller.fruit; import jakarta.validation.Valid; import me.sungbin.dto.fruit.request.SaveFruitInfoRequestDto; import me.sungbin.dto.fruit.request.UpdateFruitRequestDto; import me.sungbin.dto.fruit.response.GetFruitResponseDto; import me.sungbin.entity.fruit.Fruit; import me.sungbin.service.fruit.FruitService; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.web.bind.annotation.*; /** * @author : rovert * @packageName : me.sungbin.controller.fruit * @fileName : FruitController * @date : 2/25/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/25/24 rovert 최초 생성 */ @RestController @RequestMapping("/api/v1/fruit") public class FruitController { private final FruitService fruitService; public FruitController(FruitService fruitService) { this.fruitService = fruitService; } @PostMapping public void saveFruitInfo(@RequestBody @Valid SaveFruitInfoRequestDto requestDto) { this.fruitService.saveFruitInfo(requestDto); } @PutMapping public void updateFruitInfo(@RequestBody UpdateFruitRequestDto requestDto) { this.fruitService.updateFruitInfo(requestDto); } @GetMapping("/stat") public GetFruitResponseDto getFruitInfo(@RequestParam String name) { return this.fruitService.getFruitInfo(name); } }FruitService.javapackage me.sungbin.service.fruit; import me.sungbin.dto.fruit.request.SaveFruitInfoRequestDto; import me.sungbin.dto.fruit.request.UpdateFruitRequestDto; import me.sungbin.dto.fruit.response.GetFruitResponseDto; import me.sungbin.entity.fruit.Fruit; import me.sungbin.repository.fruit.FruitRepository; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; /** * @author : rovert * @packageName : me.sungbin.service.fruit * @fileName : FruitService * @date : 2/25/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/25/24 rovert 최초 생성 */ @Service public class FruitService { private final FruitRepository fruitRepository; public FruitService(FruitRepository fruitRepository) { this.fruitRepository = fruitRepository; } public void saveFruitInfo(SaveFruitInfoRequestDto requestDto) { Fruit fruit = requestDto.toEntity(); this.fruitRepository.saveFruitInfo(fruit); } public void updateFruitInfo(UpdateFruitRequestDto requestDto) { validate(requestDto.getId()); this.fruitRepository.updateFruitInfo(requestDto.getId()); } public GetFruitResponseDto getFruitInfo(String name) { GetFruitResponseDto fruitData = this.fruitRepository.getFruitInfo(name); validateGetFruitAmount(fruitData.getSalesAmount(), fruitData.getNotSalesAmount()); return fruitData; } private void validate(long id) { if (this.fruitRepository.isNotExistsFruitInfo(id)) { throw new IllegalArgumentException("존재하는 과일정보가 없습니다."); } } private void validateGetFruitAmount(long salesAmount, long notSalesAmount) { if (salesAmount == 0L && notSalesAmount == 0L) { throw new IllegalArgumentException("존재하는 과일이 없습니다."); } } }FruitRepository.javapackage me.sungbin.repository.fruit; import me.sungbin.dto.fruit.response.GetFruitResponseDto; import me.sungbin.entity.fruit.Fruit; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; import java.util.HashMap; import java.util.Map; import java.util.Objects; /** * @author : rovert * @packageName : me.sungbin.repository.fruit * @fileName : FruitRepository * @date : 2/25/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/25/24 rovert 최초 생성 */ @Repository public class FruitRepository { private final JdbcTemplate jdbcTemplate; public FruitRepository(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } public void saveFruitInfo(Fruit fruit) { String sql = "INSERT INTO fruit (name, warehousingDate, price) VALUES (?, ?, ?)"; this.jdbcTemplate.update(sql, fruit.getName(), fruit.getWarehousingDate(), fruit.getPrice()); } public void updateFruitInfo(long id) { String sql = "UPDATE fruit SET is_sold = 1 WHERE id = ?"; jdbcTemplate.update(sql, id); } public GetFruitResponseDto getFruitInfo(String name) { String sql = "SELECT is_sold, SUM(price) AS total FROM fruit WHERE name = ? GROUP BY is_sold"; Map<Boolean, Long> aggregatedData = jdbcTemplate.query(sql, new Object[]{name}, rs -> { HashMap<Boolean, Long> map = new HashMap<>(); while (rs.next()) { map.put(rs.getBoolean("is_sold"), rs.getLong("total")); } return map; }); long salesAmount = Objects.requireNonNull(aggregatedData).getOrDefault(true, 0L); long notSalesAmount = Objects.requireNonNull(aggregatedData).getOrDefault(false, 0L); return new GetFruitResponseDto(salesAmount, notSalesAmount); } public boolean isNotExistsFruitInfo(long id) { String readSQL = "SELECT * FROM fruit WHERE id = ?"; return jdbcTemplate.query(readSQL, (rs, rowNum) -> 0, id).isEmpty(); } }step5. postman 테스트이제 이렇게 리팩토링한 것을 postman을 이용해서 테스트해보자.현재 fruit 테이블은 아래와 같이 비어있다.과일 생성수정위의 생성 테스트가 잘 되었으니, 몇개의 데이터를 아래와 같이 만들었다.이제 2000원짜리 오렌지가 팔린 테스트를 해보겠다.조회 테스트이제 조회 테스트를 해보자. 오렌지가 팔린 금액과 안 팔린 금액을 조회해보자. step6. 테스트 코드이제 테스트 코드로 다시 한번 검증해보자. FruitControllerTest.javapackage me.sungbin.controller.fruit; import com.fasterxml.jackson.databind.ObjectMapper; import me.sungbin.dto.fruit.request.SaveFruitInfoRequestDto; import me.sungbin.dto.fruit.request.UpdateFruitRequestDto; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import java.time.LocalDate; import static org.junit.jupiter.api.Assertions.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; /** * @author : rovert * @packageName : me.sungbin.controller.fruit * @fileName : FruitControllerTest * @date : 2/25/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/25/24 rovert 최초 생성 */ @SpringBootTest @AutoConfigureMockMvc class FruitControllerTest { @Autowired private MockMvc mockMvc; @Autowired private ObjectMapper objectMapper; @Test @DisplayName("문제 1번 통합 테스트 - 실패 (가격이 음수거나 과일 이름이 공란)") void question1_test_fail_caused_by_price_is_minus_or_fruit_name_is_empty() throws Exception { SaveFruitInfoRequestDto requestDto = new SaveFruitInfoRequestDto("", LocalDate.of(2024, 1, 1), -1000); this.mockMvc.perform(post("/api/v1/fruit") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(requestDto))) .andDo(print()) .andExpect(status().isBadRequest()); } @Test @DisplayName("문제 1번 통합 테스트 - 성공") void question1_test_success() throws Exception { SaveFruitInfoRequestDto requestDto = new SaveFruitInfoRequestDto("파인애플", LocalDate.of(2024, 2, 2), 20000); this.mockMvc.perform(post("/api/v1/fruit") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(requestDto))) .andDo(print()) .andExpect(status().isOk()); } @Test @DisplayName("문제 2번 통합 테스트 - 성공") void question2_test_success() throws Exception { UpdateFruitRequestDto requestDto = new UpdateFruitRequestDto(1); this.mockMvc.perform(put("/api/v1/fruit") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(requestDto))) .andDo(print()) .andExpect(status().isOk()); } @Test @DisplayName("문제 3번 통합 테스트 - 성공") void question3_test_success() throws Exception { this.mockMvc.perform(get("/api/v1/fruit/stat") .param("name", "사과")) .andDo(print()) .andExpect(status().isOk()); } }문제2문제2는 FruitRepository를 FruitMemoryRepository 와 FruitMysqlRepository로 나누고 @Primary 어노테이션을 이용하여 두 Repository를 번갈아가며 동작시키는 것을 구현하시라고 하셨다. step1. FruitRepository 코드를 FruitMysqlRepository로 변경package me.sungbin.repository.fruit; import me.sungbin.dto.fruit.response.GetFruitResponseDto; import me.sungbin.entity.fruit.Fruit; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; import java.util.HashMap; import java.util.Map; import java.util.Objects; /** * @author : rovert * @packageName : me.sungbin.repository.fruit * @fileName : FruitRepository * @date : 2/25/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/25/24 rovert 최초 생성 */ @Repository public class FruitMysqlRepository { private final JdbcTemplate jdbcTemplate; public FruitMysqlRepository(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } public void saveFruitInfo(Fruit fruit) { String sql = "INSERT INTO fruit (name, warehousingDate, price) VALUES (?, ?, ?)"; this.jdbcTemplate.update(sql, fruit.getName(), fruit.getWarehousingDate(), fruit.getPrice()); } public void updateFruitInfo(long id) { String sql = "UPDATE fruit SET is_sold = 1 WHERE id = ?"; jdbcTemplate.update(sql, id); } public GetFruitResponseDto getFruitInfo(String name) { String sql = "SELECT is_sold, SUM(price) AS total FROM fruit WHERE name = ? GROUP BY is_sold"; Map<Boolean, Long> aggregatedData = jdbcTemplate.query(sql, new Object[]{name}, rs -> { HashMap<Boolean, Long> map = new HashMap<>(); while (rs.next()) { map.put(rs.getBoolean("is_sold"), rs.getLong("total")); } return map; }); long salesAmount = Objects.requireNonNull(aggregatedData).getOrDefault(true, 0L); long notSalesAmount = Objects.requireNonNull(aggregatedData).getOrDefault(false, 0L); return new GetFruitResponseDto(salesAmount, notSalesAmount); } public boolean isNotExistsFruitInfo(long id) { String readSQL = "SELECT * FROM fruit WHERE id = ?"; return jdbcTemplate.query(readSQL, (rs, rowNum) -> 0, id).isEmpty(); } }step2. FruitRepository 인터페이스 생성package me.sungbin.repository.fruit; import me.sungbin.dto.fruit.response.GetFruitResponseDto; import me.sungbin.entity.fruit.Fruit; /** * @author : rovert * @packageName : me.sungbin.repository.fruit * @fileName : FruitRepository * @date : 2/25/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/25/24 rovert 최초 생성 */ public interface FruitRepository { void saveFruitInfo(Fruit fruit); // 과일 생성 void updateFruitInfo(long id); // 과일 정보 업데이트 GetFruitResponseDto getFruitInfo(String name); // 과일 조회 boolean isNotExistsFruitInfo(long id); } step3. FruitMemoryRepository 생성 및 로직 개발step3-1. Fruit 클래스에 다중 생성자 추가(메모리 용 때문에)package me.sungbin.entity.fruit; import java.time.LocalDate; /** * @author : rovert * @packageName : me.sungbin.controller.fruit * @fileName : Fruit * @date : 2/25/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/25/24 rovert 최초 생성 */ public class Fruit { private long id; private String name; private LocalDate warehousingDate; private long price; private boolean isSold; public Fruit() { } public Fruit(String name, LocalDate warehousingDate, long price) { this.name = name; this.warehousingDate = warehousingDate; this.price = price; } public Fruit(String name, LocalDate warehousingDate, long price, boolean isSold) { this.name = name; this.warehousingDate = warehousingDate; this.price = price; this.isSold = isSold; } public Fruit(long id, String name, LocalDate warehousingDate, long price, boolean isSold) { this.id = id; this.name = name; this.warehousingDate = warehousingDate; this.price = price; this.isSold = isSold; } public long getId() { return id; } public String getName() { return name; } public LocalDate getWarehousingDate() { return warehousingDate; } public long getPrice() { return price; } public boolean isSold() { return isSold; } }step3-2. FruitMemoryRepository 생성 및 로직 추가package me.sungbin.repository.fruit; import me.sungbin.dto.fruit.response.GetFruitResponseDto; import me.sungbin.entity.fruit.Fruit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Repository; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; /** * @author : rovert * @packageName : me.sungbin.repository.fruit * @fileName : FruitMemoryRepository * @date : 2/25/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/25/24 rovert 최초 생성 */ @Primary @Repository public class FruitMemoryRepository implements FruitRepository { private final List<Fruit> fruits = new ArrayList<>(); private final Logger log = LoggerFactory.getLogger(this.getClass().getSimpleName()); @Override public void saveFruitInfo(Fruit fruit) { log.info("[FruitMemoryRepository] - saveFruitInfo"); fruits.add(fruit); System.out.println(fruits); } @Override public void updateFruitInfo(long id) { log.info("[FruitMemoryRepository] - updateFruitInfo"); for (int i = 0; i < fruits.size(); i++) { Fruit fruit = fruits.get(i); if (fruit.getId() == id) { // Assuming Fruit class has an appropriate constructor to handle this case Fruit updatedFruit = new Fruit(fruit.getId(), fruit.getName(), fruit.getWarehousingDate(), fruit.getPrice(), true); fruits.set(i, updatedFruit); break; } } System.out.println(fruits); } @Override public GetFruitResponseDto getFruitInfo(String name) { log.info("[FruitMemoryRepository] - getFruitInfo"); List<Fruit> filteredFruits = fruits.stream() .filter(fruit -> fruit.getName().equals(name)) .toList(); long salesAmount = filteredFruits.stream() .filter(Fruit::isSold) .mapToLong(Fruit::getPrice) .sum(); long notSalesAmount = filteredFruits.stream() .filter(fruit -> !fruit.isSold()) .mapToLong(Fruit::getPrice) .sum(); System.out.println(fruits); return new GetFruitResponseDto(salesAmount, notSalesAmount); } @Override public boolean isNotExistsFruitInfo(long id) { return fruits.stream().noneMatch(fruit -> fruit.getId() == id); } }step4. FruitMysqlRepository 수정package me.sungbin.repository.fruit; import me.sungbin.dto.fruit.response.GetFruitResponseDto; import me.sungbin.entity.fruit.Fruit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; import java.util.HashMap; import java.util.Map; import java.util.Objects; /** * @author : rovert * @packageName : me.sungbin.repository.fruit * @fileName : FruitRepository * @date : 2/25/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/25/24 rovert 최초 생성 */ @Repository public class FruitMysqlRepository implements FruitRepository { private final JdbcTemplate jdbcTemplate; private final Logger log = LoggerFactory.getLogger(this.getClass().getSimpleName()); public FruitMysqlRepository(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } @Override public void saveFruitInfo(Fruit fruit) { log.info("[FruitMysqlRepository] - saveFruitInfo"); String sql = "INSERT INTO fruit (name, warehousingDate, price) VALUES (?, ?, ?)"; this.jdbcTemplate.update(sql, fruit.getName(), fruit.getWarehousingDate(), fruit.getPrice()); } @Override public void updateFruitInfo(long id) { log.info("[FruitMysqlRepository] - updateFruitInfo"); String sql = "UPDATE fruit SET is_sold = 1 WHERE id = ?"; jdbcTemplate.update(sql, id); } @Override public GetFruitResponseDto getFruitInfo(String name) { log.info("[FruitMysqlRepository] - getFruitInfo"); String sql = "SELECT is_sold, SUM(price) AS total FROM fruit WHERE name = ? GROUP BY is_sold"; Map<Boolean, Long> aggregatedData = jdbcTemplate.query(sql, new Object[]{name}, rs -> { HashMap<Boolean, Long> map = new HashMap<>(); while (rs.next()) { map.put(rs.getBoolean("is_sold"), rs.getLong("total")); } return map; }); long salesAmount = Objects.requireNonNull(aggregatedData).getOrDefault(true, 0L); long notSalesAmount = Objects.requireNonNull(aggregatedData).getOrDefault(false, 0L); return new GetFruitResponseDto(salesAmount, notSalesAmount); } @Override public boolean isNotExistsFruitInfo(long id) { String readSQL = "SELECT * FROM fruit WHERE id = ?"; return jdbcTemplate.query(readSQL, (rs, rowNum) -> 0, id).isEmpty(); } }step5. FruitService 수정package me.sungbin.service.fruit; import me.sungbin.dto.fruit.request.SaveFruitInfoRequestDto; import me.sungbin.dto.fruit.request.UpdateFruitRequestDto; import me.sungbin.dto.fruit.response.GetFruitResponseDto; import me.sungbin.entity.fruit.Fruit; import me.sungbin.repository.fruit.FruitMysqlRepository; import me.sungbin.repository.fruit.FruitRepository; import org.springframework.stereotype.Service; /** * @author : rovert * @packageName : me.sungbin.service.fruit * @fileName : FruitService * @date : 2/25/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/25/24 rovert 최초 생성 */ @Service public class FruitService { private final FruitRepository fruitRepository; public FruitService(FruitRepository fruitRepository) { this.fruitRepository = fruitRepository; } public void saveFruitInfo(SaveFruitInfoRequestDto requestDto) { Fruit fruit = requestDto.toEntity(); this.fruitRepository.saveFruitInfo(fruit); } public void updateFruitInfo(UpdateFruitRequestDto requestDto) { validate(requestDto.getId()); this.fruitRepository.updateFruitInfo(requestDto.getId()); } public GetFruitResponseDto getFruitInfo(String name) { GetFruitResponseDto fruitData = this.fruitRepository.getFruitInfo(name); validateGetFruitAmount(fruitData.getSalesAmount(), fruitData.getNotSalesAmount()); return fruitData; } private void validate(long id) { if (this.fruitRepository.isNotExistsFruitInfo(id)) { throw new IllegalArgumentException("존재하는 과일정보가 없습니다."); } } private void validateGetFruitAmount(long salesAmount, long notSalesAmount) { if (salesAmount == 0L && notSalesAmount == 0L) { throw new IllegalArgumentException("존재하는 과일이 없습니다."); } } }step6. postman 테스트 현재 @Primary 어노테이션을 FruitMemoryRepository로 붙여두고 테스트를 해보았다.생성 (메모리) 수정 (메모리)조회 (메모리)이제 FruitMysqlRepository로 이용해보자! FruitMemoryRepository의 @Primary 어노테이션을 지워주고 FruitMysqlRepository에 붙여주자!package me.sungbin.repository.fruit; import me.sungbin.dto.fruit.response.GetFruitResponseDto; import me.sungbin.entity.fruit.Fruit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Primary; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; import java.util.HashMap; import java.util.Map; import java.util.Objects; /** * @author : rovert * @packageName : me.sungbin.repository.fruit * @fileName : FruitRepository * @date : 2/25/24 * @description : * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * 2/25/24 rovert 최초 생성 */ @Primary @Repository public class FruitMysqlRepository implements FruitRepository { private final JdbcTemplate jdbcTemplate; private final Logger log = LoggerFactory.getLogger(this.getClass().getSimpleName()); public FruitMysqlRepository(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } @Override public void saveFruitInfo(Fruit fruit) { log.info("[FruitMysqlRepository] - saveFruitInfo"); String sql = "INSERT INTO fruit (name, warehousingDate, price) VALUES (?, ?, ?)"; this.jdbcTemplate.update(sql, fruit.getName(), fruit.getWarehousingDate(), fruit.getPrice()); } @Override public void updateFruitInfo(long id) { log.info("[FruitMysqlRepository] - updateFruitInfo"); String sql = "UPDATE fruit SET is_sold = 1 WHERE id = ?"; jdbcTemplate.update(sql, id); } @Override public GetFruitResponseDto getFruitInfo(String name) { log.info("[FruitMysqlRepository] - getFruitInfo"); String sql = "SELECT is_sold, SUM(price) AS total FROM fruit WHERE name = ? GROUP BY is_sold"; Map<Boolean, Long> aggregatedData = jdbcTemplate.query(sql, new Object[]{name}, rs -> { HashMap<Boolean, Long> map = new HashMap<>(); while (rs.next()) { map.put(rs.getBoolean("is_sold"), rs.getLong("total")); } return map; }); long salesAmount = Objects.requireNonNull(aggregatedData).getOrDefault(true, 0L); long notSalesAmount = Objects.requireNonNull(aggregatedData).getOrDefault(false, 0L); return new GetFruitResponseDto(salesAmount, notSalesAmount); } @Override public boolean isNotExistsFruitInfo(long id) { String readSQL = "SELECT * FROM fruit WHERE id = ?"; return jdbcTemplate.query(readSQL, (rs, rowNum) -> 0, id).isEmpty(); } }  생성 (Mysql)수정 (MySQL)조회 (Mysql)회고오늘의 강의 핵심은 의존성 주입과 제어의 역전이었다. 나는 기존에 이런 개념들이 뭔지는 대강 알고는 있었지만 확실히 강의와 이렇게 실습함으로 뭔가 체득이 되었다. 아직 많이 부족한 부분이 있을테니 나 따로 더 연습을 해봐야겠다. 

백엔드인프런워밍업스터디클럽백엔드DIIoC

스프링 핵심원리 기본편(김영한) 1 - 객체지향 DIP와 스프링 DI, IoC

  객체는 객체와 끊임없이 상호작용한다. 그렇기에 유연한 변경이 가능해야한다. 예를 들어, 자동차라는 상위 클래스를 다양한 자동차 브랜드로 구현될 수 있고, 운전자가 변화해도 자동차는 영향을 받지 않는다. 사용자, 주문, 할인 등 여러 독립적인 특징을 가진 기능은 클래스로 분리하여 각 클래스에서만 수정 및 사용한다.   역할과 구현을 분리 - 인터페이스와 콘크리트 클래스 인터페이스는 안정적이게, 확장이 무한대로 가능하게 설계해야한다.   SOLID 객체지향 설계 원칙 1. SRP 단일책임원칙 - 변경이 용이한 단위적 책임인가2. OCP 개방폐쇄원칙 - 코드의 변경 없이 확장이 가능한가(조립만으로 변경)3. LSP 리스코프 치환 원칙 - 하위 클래스는 인터페이스(상위 클래스)를 위반하지 않아야한다4. ISP 인터페이스 분리 원칙 - 여러 개의 인터페이스를 통해 명확한 기능을 갖고 있고, 대체 가능성이 높은 환경을 구현할 것5. DIP 의존관계 역전 원칙 - 추상화에 의존할 것, 인터페이스(역할)가 중심이 되어야한다. 구현체에 의존하면 다형성을 잃는다(재활용성을 잃는다) 스프링 컨테이너에 객체 지향 적용 객체를 생성하는 역할과 객체를 실행하는 역할을 분리.의존은 인터페이스로 하고, 설정 파일을 통해 구체적인 구현체를 의존 주입구현체 변경 시 설정 파일만 변경하면 된다.(조립)=> 제어의 역전; 어떤 구현체를 사용할 것인지 AppConfig(Spring)가 결정한다. 동적인 인스턴스 의존관계    

객체지향javaSOLIDspringDIIoCDIP강의김영한

채널톡 아이콘