@RestController
@RequestMapping("/member")
@RequiredArgsConstructor
public class MemberController {
private final MemberService service;
@PostMapping
public void saveMember(@Valid @RequestBody MemberSaveRequest request) {
service.saveMember(request);
}
}
Service
@Service
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository repository;
@Transactional
public void saveMember(MemberSaveRequest request) {
repository.save(new Employee(request));
}
}
repository
public interface MemberRepository extends JpaRepository<Employee,Long> {
Employee findByName(String name);
}
@RestController
@RequestMapping("/team")
public class TeamController {
private final TeamService service;
public TeamController(TeamService service) {
this.service = service;
}
@PostMapping
public void registerTeam(@RequestBody Map<String, String> requestJson){
service.registerTeam(requestJson.get("teamName"));
}
}
Repository
public interface TeamRepository extends JpaRepository<Team,Long> {
Team findByTeamName(String teamName);
//팀 이름을 조회하여 있으면 true 없으면 false를 리턴
Boolean existsByTeamName(String teamName);
}
Service
@Service
public class TeamService {
private final TeamRepository repository;
public TeamService(TeamRepository repository) {
this.repository = repository;
}
public void registerTeam(String teamName) {
//이미 있는 팀이름을 등록하는 경우 에러처리
//팀 이름을 키로 지정해도 같은 문제를 해결할수있을것같다.
if(isExistTeam(teamName)){
throw new IllegalArgumentException("이미 존재하는 팀 이름입니다: " + teamName);
}
repository.save(new Team(teamName));
}
private boolean isExistTeam(String name){
return repository.existsByTeamName(name);
}
}
디자인팀이 데이터베이스에 잘 저장되었다.
같은 이름의 팀을 저장해보자.
동일한 이름의 팀을 저장하려고 시도하면
에러가 발생한다.
이미 존재하는 팀의 이름을 저장하려고 하면 에러가 발생하는것을 볼수있다.
api응답으로도 에러메시지를 받을수있는데 이경우에는 controller에서 ResponseEntity를 리턴해줘야하는것 같다.
간단한 저장이라서 응답없이 처리하도록 해주었기때문에
api응답은 에러의 발생원인이 서버에게있다고하는 500에러를 보내게된다.
여기서 문제가 발생한다. 에러를 발생시킨부분은 서버가 맞지만 잘못된 요청으로 인해 에러가 발생한것으로
다시 잘 요청해달라는 의미로 클라이언트 에러인 400번대 에러를 보내줘야한다. (클라이언트 너의 책임이다!)
여기서 스프링 Controller의 에러처리 기능인 @RestControllerAdvice를 사용해주었다. (처음 써봤다.)
@RestControllerAdvice , @ControllerAdvice
두 어노테이션 모두 스프링에서 Controller의 전역적인 에러처리를 위해서 사용된다.
@ControllerAdvice
페이지가 있는 컨트롤러에서 이용되며
주로 에러페이지로 이동하는데 사용한다고 한다(아직 안써봄)
@RestController
에러메시지를 Json으로 내려줄수있어서 api를 개발할때 사용한다(적용).
RestControllerAdvice는 ControllerAdvice를 포함하고있다
@RestControllerAdvice
public class commuteServiceException {
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<String> illegalArgumentExceptionHandle(IllegalArgumentException e) {
return ResponseEntity.badRequest().body("error : " + e.getMessage());
}
}
이렇게 에러를 처리해줄 클래스를 만들고 @RestControllerAdvice어노테이션을 달아주면
컨트롤러에서 IllegarArgumentException이 발생했을때 모두 여기서 처리해서 에러응답을 보내주게된다.
에러코드는 400으로 나온다. badRequest
이제 팀을 저장하는 기능이끝났다.
2. 팀장 등록
추가적인 기능을 위해
팀 테이블에 팀장의 컬럼을 추가시켜서
이 팀의 팀장이 누구인지의 데이터를 가질수있게 처리해줬다.
팀을 저장할때는 팀의 이름만 저장하기때문에 팀장값은 null로 가지고있을것이다.
이제 이 팀장을 지정해주는 기능을 만들어주어야한다.
요구사항
1. 직원의 정보를 보내서 팀장을 저장할수있게해준다.
예외처리.
x.
따로 예외처리를 해주지는 않았지만 나중에 비즈니스 요구사항에 따라 다양한 예외처리가 추가될수있을것같다.
특정 직급이상만 팀장으로 등록이 가능하다거나.
특정 직원은 등록이 불가능하게 처리한다거나 할수있을듯하다.
(현재는 딱히 필요없어보여서 생략)
//DTO
public class TeamSetDto {
private String teamName;
private Integer leaderCode; // 팀장의 이름이나 id 둘중의 하나만 가지면 될것같음
//private String leaderName;
}
//jpa를 통해서 id 나 이름이 있다면 직원정보를 조회할수있기때문에 두개중에 하나만있다면
//조회해서 다른정보를 불러서 사용이 가능하다.
//Controller
@PostMapping("/setLeader")
public void setTeamLeader(@RequestBody TeamSetDto request){
service.registerTeamLeader(request);
}
//Service
@Transactional
public void registerTeamLeader(TeamSetDto request) {
Team team = repository.findByTeamName(request.getTeamName());
team.setTeamLeader(request.getLeaderCode());
}
//팀원의 이름이나 정보로조회해서 저장하는경우.
@Transactional
public void registerTeamLeader(TeamSetDto request) {
Team team = repository.findByTeamName(request.getTeamName());
Employee employee = memberRepository.findByName(request.getName());
team.setTeamLeader(employee.getId());
}
각 계층에서 추가한부분만 옮겨봤다.
팀의 정보와 팀장으로저장할 직원의 정보를받아서 팀에 넣어주면
영속성컨텍스트에의해 조회한 team 인스턴스가 변경된걸 캐치해서
자동으로 업데이트 해주게된다.
개인적인 생각
지금은 그냥 postman을 통해서 요청값을 세팅하기때문에 문자열정보인 String이 더 편하지만
추후에 개발시에 프론트를 구성한다면 직원정보를 조회하고
보이는쪽은이름 실제 보내지는 value쪽은 직원의 id값이 들어가서
api에 id로 요청하는게 더 수월해지기때문에 id가 필요한 api로 개발하는게 더 좋은것같다.
조회는 id로 해야 db의 index로 인해서 더 빠르다는 얘기도 들어본것같은...
3. 팀 조회기능
//DTO
@Getter
@AllArgsConstructor // Jpa매핑용 , 모든파라미터생성자
public class TeamResponse {
private String name;
private String manager;
private Long count;
}
//Controller
@GetMapping
public List<TeamResponse> getTeam(){
return service.getTeam();
}
//Service
public List<TeamResponse> getTeam(){
return repository.getAllTeam();
}
//Repository
// Jpql사용
@Query("SELECT new Company.demo.commute.dto.response.TeamResponse(a.teamName, b.name, COUNT(c.id)) " +
"FROM Team a " +
"LEFT JOIN Employee b ON a.teamLeader = b " +
"LEFT JOIN Employee c ON a.teamId = c.team.teamId " +
"GROUP BY a.teamName, b.name")
List<TeamResponse> getAllTeam();
조회결과.
이번에는 Jpql로 데이터를 조회했다.
jpa로 응답객체를 내주는데에는 여러가지방법이있는데 간단한조회라면
그냥 객체를 findby로 조회해서 서비스레이어에서 매핑해서 응답객체를 내주는것도 괜찮은 방법인것같다.
여기서는 팀장의 이름을 알아내기위해 직원 테이블도 조인해줘야하고 팀의 총인원을 집계해줘야해서
단순조회로는 코드가 너무 길어질것같아서 쿼리로 처리해 주었다.
Jpql에서 join을 사용하려면 Entity객체에서도 관계설정이 되어있어야한다.
이로인해서 기존 Entity코드에도 변경이 생겼다.
Team (Entity)
@Entity
@Getter
@NoArgsConstructor
public class Team {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long teamId;
private String teamName;
@OneToOne
@JoinColumn(name = "team_leader_id")
private Employee teamLeader;
//private Integer teamLeaderId;
private Integer annualLeaveBefore;
@OneToMany(mappedBy = "team")
private List<Employee> employeeList;
public Team(String positionName) {
this.teamName = positionName;
}
public void setTeamLeader(Employee leader) {
this.teamLeader = leader;
}
// public void setTeamLeader(Integer leaderCode){
// this.teamLeaderId = leaderCode;
// }
}
Team 엔티티 객체에서 기존에 단순히 팀장코드를 가지고있던 필드에 관계설정을 해줘서 직원객체랑 매핑해주었다.
@Service
public class FruitServiceImpl implements FruitService{
private final FruitRepository repository;
public FruitServiceImpl(@Qualifier("first")FruitRepository repository) {
this.repository = repository;
}
@Override
public void saveFruit(String name, String date, Integer price) {
repository.saveFruit(name,date,price);
}
@Override
public void updateFruit(long id) {
if(repository.isFruitExist(id)){
throw new IllegalArgumentException("수정할 자료가 없습니다.");
}
repository.updateFruit(id);
}
@Override
public FruitShopAmoutResponse getFruitStat(String name) {
return repository.getFruitStat(name);
}
}
FruitRepository
(기존코드)
@Repository
public class FruitSQLRepository implements FruitRepository {
private final JdbcTemplate template;
public FruitSQLRepository(JdbcTemplate jdbcTemplate) {
this.template = jdbcTemplate;
}
@Override
public void saveFruit(String name, String date, Integer price) {
String sql = "insert into fruitshop (name,warehousedate,price) values (?,?,?)";
template.update(sql, name, date, price);
}
@Override
public void updateFruit(long id) {
String sql = "update fruitshop set sell = true where id = ?";
template.update(sql, id);
}
@Override
public boolean isFruitExist(long id) {
String readSql = "select id from fruitshop where id = ?";
return template.query(readSql, (rs, rowNum) -> 0, id).isEmpty();
}
@Override
public FruitShopAmoutResponse getFruitStat(String name) {
String sql = "SELECT sell, SUM(price) as totalAmount FROM fruitshop WHERE name = ? GROUP BY sell";
AtomicLong salesAmount = new AtomicLong(0);
AtomicLong notSalesAmount = new AtomicLong(0);
template.query(sql, new Object[]{name}, (rs, rowNum) -> {
boolean status = rs.getBoolean("sell");
long totalAmount = rs.getLong("totalAmount");
if (status) {
salesAmount.addAndGet(totalAmount);
} else {
notSalesAmount.addAndGet(totalAmount);
}
return null;
});
return new FruitShopAmoutResponse(salesAmount.get(), notSalesAmount.get());
}
}
이전에 만들었던 SQL쿼리를 이용한 Repository와 Service를
Jpa를 이용한 케이스로 수정.
Fruit (DAO) - 테이블과 매칭되는 DAO객체에 @Entity와 @ID @Column으로 매칭
@Entity
public class Fruit {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id = null;
@Column(nullable = false, length = 20)
private String name;
private Integer price; //타입 필드이름 이 테이블과 일치하면 @Column은 생략가능.
private String wareHouseDate; // 테이블의 필드이름 ware_house_date
private boolean sell = false;
private static long seq = 1;
public Fruit(String name, Integer price, String wareHouseDate) {
this.name = name;
this.price = price;
this.wareHouseDate = wareHouseDate;
}
public Fruit() {
}
public String getName() {
return name;
}
public long getPrice() {
return price;
}
public String getWareHouseDate() {
return wareHouseDate;
}
public void setSell(){
this.sell = true;
}
}
객체의 이름은 테이블과 매칭되어야하고
객체의 필드이름을 카멜케이스로 쓰게되면 테이블의 필드는 스네이크케이스로작성되어있어야한다.
만약 객체의 이름이 테이블의 이름과 다르다면@Entity(name = "테이블이름")으로 사용하면된다.
Repository (Interface)
public interface FruitRepositoryJpa extends JpaRepository<Fruit, Long> {
//아래는 추상메서드 기본제공되는 메서드이외에 필요시 추가해서 사용이 가능하다.
//메서드의 이름의로 쿼리가 자동으로 작성되어 구현되고
//쿼리가 복잡하여 메서드의이름으로 구현이안되는경우 Query어노테이션을 통해서 쿼리를 사용할수있다.
@Query(value = "SELECT sell, SUM(price) as totalAmount FROM fruit WHERE name = :name GROUP BY sell", nativeQuery = true)
List<Object> findSellAndTotalAmountByName(@Param("name") String name);
Long countByName(String name);
List<Fruit> findByPriceGreaterThan(Integer price);
List<Fruit> findByPriceLessThan(Integer price);
}
@Service
public class FruitServiceJpa implements FruitService {
private final FruitRepositoryJpa repository;
//Jpa를 상속받은 repository를 주입받는다.
public FruitServiceJpa(FruitRepositoryJpa repository) {
this.repository = repository;
}
@Override
public void saveFruit(String name, String date, Integer price) {
repository.save(new Fruit(name, price, date));
}
@Override
public void updateFruit(long id) {
Fruit fruit = repository.findById(id).orElseThrow(IllegalArgumentException::new);
fruit.setSell();
repository.save(fruit);
}
@Override
public FruitShopAmoutResponse getFruitStat(String name) {
return mapToAmount(repository.findSellAndTotalAmountByName(name));
}
public Map<String, Long> getFruitCount(String name) {
long count = repository.countByName(name);
Map<String, Long> resultMap = new HashMap<>();
resultMap.put("count", count);
return resultMap;
}
private FruitShopAmoutResponse mapToAmount(List<Object> result) {
AtomicLong salesAmount = new AtomicLong(0);
AtomicLong notSalesAmount = new AtomicLong(0);
result.stream()
.map(obj -> (Object[]) obj)
.forEach(rs -> {
boolean sell = (boolean) rs[0];
long totalAmount = ((BigDecimal) rs[1]).longValue();
if (sell) {
salesAmount.addAndGet(totalAmount);
} else {
notSalesAmount.addAndGet(totalAmount);
}
});
return new FruitShopAmoutResponse(salesAmount.get(), notSalesAmount.get());
}
public List<FruitResponse> getListCondition(String condition, Integer price) {
if (condition.equals("GTE")) {
// select * from fruit where price > 16000
return repository.findByPriceGreaterThan(price).stream()
.map(fruit -> new FruitResponse(fruit.getName(), Integer.valueOf((int) fruit.getPrice()),
fruit.getWareHouseDate()))
.collect(Collectors.toList());
} else{
return repository.findByPriceLessThan(price).stream()
.map(fruit -> new FruitResponse(fruit.getName(), Integer.valueOf((int) fruit.getPrice()), fruit.getWareHouseDate()))
.collect(Collectors.toList());
}
}
}
Service에서 사용하는 repository를 Jpa를 상속받는 repositoryJpa로 주입받아서
repository를 통해 Jpa의 기능을 사용할수있다.
해당 인터페이스의 구현체를 따로구현하지않아도 구현체가 자동으로 생성되어주입되고
JpaRepository를 상속받으면서 빈으로도 자동등록되기때문에 따로 처리를 해줄필요도 없다. (간편)
기본 save나 update문 들은 Jpa에 구현되어있는 기능으로 만들수있었는데
getFruitStat은 조건과 그룹바이가 달려있어서 기본 쿼리만으로는 사용할수없어서
인터페이스에 따로 추상메서드를 만들어주었다.
@Query(value = "SELECT sell, SUM(price) as totalAmount FROM fruit WHERE name = :name GROUP BY sell", nativeQuery = true)
List<Object> findSellAndTotalAmountByName(@Param("name") String name);
@Query어노테이션 을 통해서 기존에 사용하던 쿼리문을 사용할수있다.
name으로 조회해서 sell필드로 그룹을 만들어서 그룹의 가격을 List<Object>로 리턴한다.
@Override
public FruitShopAmoutResponse getFruitStat(String name) {
return mapToAmount(repository.findSellAndTotalAmountByName(name));
}
private FruitShopAmoutResponse mapToAmount(List<Object> result) {
AtomicLong salesAmount = new AtomicLong(0);
AtomicLong notSalesAmount = new AtomicLong(0);
result.stream()
.map(obj -> (Object[]) obj)
.forEach(rs -> {
boolean sell = (boolean) rs[0];
long totalAmount = ((BigDecimal) rs[1]).longValue();
if (sell) {
salesAmount.addAndGet(totalAmount);
} else {
notSalesAmount.addAndGet(totalAmount);
}
});
return new FruitShopAmoutResponse(salesAmount.get(), notSalesAmount.get());
}
해당 쿼리를 통해서 List<Object>로 리턴되는값을 매핑해서
FruitShowAmountResponse객체로 만들어서 리턴해준다.
새로운 API를 추가하고 Jpa를 통해서 기능을 구현.
API 스펙
해당 API의 진입지점을 먼저 Controller에 만들어주자.
Controller
@GetMapping("/count") // api/v1/fruit 은 RequestMappint에서 명시되어있다.
public Map<String,Long> getFruitCount(@RequestParam String name){
return service.getFruitCount(name);
}
"count": long인 Json을 리턴해주기위해서 Map타입을 리턴해주도록 했다.
Repository
public Map<String, Long> getFruitCount(String name) {
long count = repository.countByName(name);
Map<String, Long> resultMap = new HashMap<>();
resultMap.put("count", count);
return resultMap;
}
파라미터로 들어오는 이름으로 조회해서 갯수를 세는 쿼리를 만들어야하는데
Jpa는 interface에 있는 메서드의 이름으로 해당 쿼리를 구현해서 자동으로 만들어준다,
Long countByName(String name); //select count(*) as count from fruit where name = ?
이렇게 인터페이스에 countByName이라는 함수를 만드는것만으로 기능을 구현할수있다.
메서드의 이름을통해서 쿼리를 만드는거라고 생각하면 될것같다.
여기도 규칙이있는데 이 규칙은 필요할때 찾아보면서 익숙해지면 편하게 사용할수있을것같다.
http://localhost:8080/api/v1/fruit/count?name=사과
이 문제도 Jpa를 통해서 쿼리를 만들지않고 추상메서드를 만드는것으로 해결할수있었다.
여기서는 조건에 따라 다른 쿼리문을 실행시켜줘야한다.
SQL문이었다면 쿼리문을 두개만들어놓고 조건에 따라 다른 쿼리를 실행시켜주거나
쿼리문 중간의 조건을 바꿔치기 하는방법으로 사용할수있을것같다.
Jpa에서는 쿼리문을 만들지않아도되기때문에 간단하게 추상 메서드를 두개 만들어서 사용해주었다.
@RestController
@RequestMapping("/api/v1/fruit")
public class FruitShopController {
private final JdbcTemplate template;
public FruitShopController(JdbcTemplate template) {
this.template = template;
}
@PostMapping
public void saveFruit(@RequestBody FruitSaveRequest request) {
String sql = "insert into fruitshop (name,warehousedate,price) values (?,?,?)";
template.update(sql, request.getName(), request.getWarehousingDate(), request.getPrice());
}
@PutMapping
public void sellFruit(@RequestBody Map<String, Object> requestJson) {
long id = Long.parseLong(requestJson.get("id").toString());
String readSql = "select id from fruitshop where id = ?";
Boolean isUserNotExist = template.query(readSql, (rs, rowNum) -> 0, id).isEmpty();
if (isUserNotExist) {
throw new IllegalArgumentException("수정할 자료가 없습니다.");
}
String sql = "update fruitshop set sell = true where id = ?";
template.update(sql, id);
}
@GetMapping("/stat")
public FruitShopAmoutResponse getFruitStat(@RequestParam("name") String name) {
String sql = "SELECT sell, SUM(price) as totalAmount FROM fruitshop WHERE name = ? GROUP BY sell";
AtomicLong salesAmount = new AtomicLong(0);
AtomicLong notSalesAmount = new AtomicLong(0);
template.query(sql, new Object[]{name}, (rs, rowNum) -> {
boolean status = rs.getBoolean("sell");
long totalAmount = rs.getLong("totalAmount");
if (status) {
salesAmount.addAndGet(totalAmount);
} else {
notSalesAmount.addAndGet(totalAmount);
}
return null;
});
return new FruitShopAmoutResponse(salesAmount.get(), notSalesAmount.get());
}
}
문제
각 메서드마다 여러개의 역할을 가지고있는걸알수있다.
Controller에서는 api의 진입지점과 어떤 동작을 하는지 명확하게 알수있게 정리하고
SQL문은 repository에서 처리하고
에러처리 나 추가적인 로직은 service에서 처리할수 있도록 메서드를 계층별로 분리해야한다.
각 메서드를 분리한 예시
한 개로 되어있는 메서드를
SQL부분은 repostory에서
에러처리와 repository의 메서드 호출은 service에서
Controller 에서는 service 계층의 메서드만 호출해준다.
완성 코드
Controller
@RestController
@RequestMapping("/api/v1/fruit")
public class FruitShopController {
private FruitService service;
public FruitShopController(FruitService service) {
this.service = service;
}
@PostMapping
public void saveFruit(@RequestBody FruitSaveRequest request) {
service.saveFruit(request.getName(),request.getWarehousingDate(),request.getPrice());
}
@PutMapping
public void updateFruit(@RequestBody Map<String, Object> requestJson) {
long id = Long.parseLong(requestJson.get("id").toString());
service.updateFruit(id);
}
@GetMapping("/stat")
public FruitShopAmoutResponse getFruitStat(@RequestParam("name") String name) {
return service.getFruitStat(name);
}
}
Service
public interface FruitService {
void saveFruit(String name, String date, Long price);
void updateFruit(long id);
FruitShopAmoutResponse getFruitStat(String name);
}
@Service
public class FruitServiceImpl implements FruitService{
private final FruitRepository repository;
public FruitServiceImpl(FruitRepository repository) {
this.repository = repository;
}
@Override
public void saveFruit(String name, String date, Long price) {
repository.saveFruit(name,date,price);
}
@Override
public void updateFruit(long id) {
if(repository.isFruitExist(id)){
throw new IllegalArgumentException("수정할 자료가 없습니다.");
}
repository.updateFruit(id);
}
@Override
public FruitShopAmoutResponse getFruitStat(String name) {
return repository.getFruitStat(name);
}
}
@Repository
public class FruitSQLRepository implements FruitRepository {
private final JdbcTemplate template;
public FruitSQLRepository(JdbcTemplate jdbcTemplate) {
this.template = jdbcTemplate;
}
@Override
public void saveFruit(String name, String date, Long price) {
String sql = "insert into fruitshop (name,warehousedate,price) values (?,?,?)";
template.update(sql, name, date, price);
}
@Override
public void updateFruit(long id) {
String sql = "update fruitshop set sell = true where id = ?";
template.update(sql, id);
}
@Override
public boolean isFruitExist(long id) {
String readSql = "select id from fruitshop where id = ?";
return template.query(readSql, (rs, rowNum) -> 0, id).isEmpty();
}
@Override
public FruitShopAmoutResponse getFruitStat(String name) {
String sql = "SELECT sell, SUM(price) as totalAmount FROM fruitshop WHERE name = ? GROUP BY sell";
AtomicLong salesAmount = new AtomicLong(0);
AtomicLong notSalesAmount = new AtomicLong(0);
template.query(sql, new Object[]{name}, (rs, rowNum) -> {
boolean status = rs.getBoolean("sell");
long totalAmount = rs.getLong("totalAmount");
if (status) {
salesAmount.addAndGet(totalAmount);
} else {
notSalesAmount.addAndGet(totalAmount);
}
return null;
});
return new FruitShopAmoutResponse(salesAmount.get(), notSalesAmount.get());
}
}
이제 컨트롤러는 api의 진입점 역활을 하고 어떤동작을 하는지 한눈에 파악하기 쉬워졌다.
그리고 JdbcTemplate을 의존하지않고 Service클래스만 의존해서 사용하게된다.
스프링 컨테이너 & 스프링 빈
각 계층에서 생성자를 주입받을때 인스턴스를 생성하지 않고 자동으로 생성이 되는걸 알수있다.
이는 스프링에서 지원하는 스프링 컨테이너와 스프링 빈 덕분이다.
각 클래스의 위에 붙어있는 @RestController , @Service , @Repostory 덕분에
스프링은 이 객체들을 빈으로 인식하고 시작되는시점에 컨테이너에 이 객체들을 생성해서 보관하게된다
이로 인해서 필요할때에 객체를 생성하지않고 자동으로 주입해서 사용할수있다.
JdbcTemplate객체의 경우에는 스프링에서 기본으로 등록되어있는 빈이기때문에 따로 설정을 해주지않아도 주입받아서 사용할수있다 이를 의존성주입 (Dependency Injection) 이라고 한다.
나는 서비스와 저장소 객체를 인터페이스로 만들고 그 구현체를 만드는 방법으로 사용했는데
이렇게 하면 Controller나 Service에서 생성자에 주입받을때에 interface객체를 타입으로 넣게되면
스프링컨테이너에서 구현체인 빈을 자동으로 주입해주기 때문에 추후에 구현체를 변경해야할일이 생겼을때
구현체의 코드만 수정해주면 Service계층이나 Controller계층의 코드는 수정하지 않고
자동으로 구현체가 변경되어서 주입된다.
ex) MemoryRepository
@Primary
@Repository
public class FruitMemoryRepository implements FruitRepository{
List<Fruit> fruitRepository = new ArrayList<>();
@Override
public void saveFruit(String name, String date, Long price) {
fruitRepository.add(new Fruit(name,price,date));
}
@Override
public void updateFruit(long id) {
fruitRepository.set((int) id,new Fruit("수정",000,"00-00-00"));
}
@Override
public boolean isFruitExist(long id) {
if(fruitRepository.get((int)id) == null){
return false;
}
return true;
}
@Override
public FruitShopAmoutResponse getFruitStat(String name) {
return new FruitShopAmoutResponse(0,fruitRepository.stream().mapToLong(Fruit::getPrice).sum());
}
}
이렇게 Repository의 구현체를 하나 더 만들고 @Repository 어노테이션을 붙여주면
스프링에서는 어떤 구현체를 주입해야할지 몰라서 에러가 발생하게된다.
이럴때는 @Primary어노테이션을 주입할 객체에 붙여서 우선순위를 만들어주거나
@Qulifier 어노테이션을 이용해서 주입할 빈의 이름을 지정하고
주입받는곳에서 파라미터에 @Qualifier 어노테이션을 같이붙여서 가져오고싶은 빈의 이름을 넣어서 받아올수도있다.
@Qualifier("first") // 빈의 이름을 설정
@Repository
public class FruitMemoryRepository implements FruitRepository{
@Service
public class FruitServiceImpl implements FruitService{
private final FruitRepository repository;
public FruitServiceImpl(@Qualifier("first")FruitRepository repository) { //주입시 이름으로 지정해서 받아옴
this.repository = repository;
}
public class CalculatorAddRequest {
private final int number1;
private final int number2;
public CalculatorAddRequest(int number1, int number2) {
this.number1 = number1;
this.number2 = number2;
}
public int getNumber1() {
return number1;
}
public int getNumber2() {
return number2;
}
}
이 객체는 파라미터를 받기위한 객체인데 강의중에 만든 객체지만
숫자 파라미터를 두개받는 객체로 적절해보여서 그냥 재사용했다.
public class CalcResult {
private int add;
private int minus;
private int multiply;
public CalcResult(CalculatorAddRequest request) {
this.add = request.getNumber1() + request.getNumber2();
this.minus = request.getNumber1() - request.getNumber2();
this.multiply = request.getNumber1() * request.getNumber2();
}
public int getAdd() {
return add;
}
public int getMinus() {
return minus;
}
public int getMultiply() {
return multiply;
}
}
이 객체는 계산결과를 응답으로 내보내기 위해서 새로 생성한 객체이다.
생성시에 CalculatorAddRequest객체를 받도록 만들었는데
의존을 줄이려면 그냥 int타입 파라미터 두개로 받아서 만드는것도 괜찮아보인다.
그리고 처음에는 필드와 생성자만 만들어서 사용했는데 요청을 보내보니 응답이 에러가 나는 상황이 생겼다.
찾아보니 getter메서드가 있어야 결과를 받아서 직렬화하여 Json객체로 응답을 보낼수있다는듯 하다.
@GetMapping("/api/v1/calc")
public CalcResult returnResult(CalculatorAddRequest request) {
return new CalcResult(request);
}