반응형

 

이번 과제는 고민을 좀 많이 하면서 했다 출근과 퇴근을 분리해야할지 하나로 합쳐서 안에서 분기로 처리할지.

결국에는 하나로 합쳐서 데이터에따라 분기로 처리해주었다

 

출퇴근 기능

1. 아이디를 파라미터로 받아서 직원정보를 조회하고 출퇴근 기록을 저장

2. 데이터를 조회해서 당일 출퇴근 기록이없는경우 새로 출퇴근기록을 저장

3. 데이터를 조회해서 당일 출근기록은 있는데 퇴근이없는경우 퇴근시간을 업데이트

4. 데이터를 조회해서 당일 출퇴근기록이 전부있는경우 새로 출근기록을 저장.

 

예외처리

1. 등록되지않은 직원이 출근 (직원이 조회되지않음) 

2. 출근한 직원이 다시 출근 

3. 퇴근하려는 직원이 출근하지않았던 경우

4. 그 날, 출근했다 퇴근한 직원이 다시 출근하려는경우

 

예외의 2번과 3번은 서비스계층내부에서 분기로 처리하면서 자동으로 처리가 된다 (출근 퇴근을 따로등록할수없음)

4번의 경우는 출근하고 퇴근했다가 다시 출근해서 추가적으로 근무를 해야하는경우도 있을것같아서 막지는 않았다.

추가적으로 출근기록을 생성하도록 처리해주었다. 

 

Controller

@RestController
@RequestMapping("/commute")
public class CommuteController {
    private final CommuteService service;

    public CommuteController(CommuteService service) {
        this.service = service;
    }

    @PostMapping
    public void checkWork(@RequestBody Map<String, Long> json) {
        service.checkWork(json.get("employeeId"));
    }
}

Service

@Service
@RequiredArgsConstructor
@Slf4j
public class CommuteService {

    private final CommuteHistoryRepository repository;
    private final MemberRepository memberRepository;

    @Transactional
    public void checkWork(Long employeeId) {
        //해당 아이디의 직원을 조회
        //예외사항 1번에대해서 예외를 발생시킴 
        Employee employee = memberRepository.findById(employeeId).orElseThrow(()-> new IllegalArgumentException("직원 정보가없습니다."));
        //직원의 금일 근무정보가 있는지 조회
        EmployeeTodayCommuteList employeeTodayCommuteList = new EmployeeTodayCommuteList(employee);
        checkStartOrEndWorking(employeeTodayCommuteList, employee);
    }

    private void checkStartOrEndWorking(EmployeeTodayCommuteList employeeTodayCommuteList, Employee employee) {
        // 금일근무정보가 없거나 ,근무기록은 있지만 근무중이 아닌경우(이미퇴근) ,새로 출근기록 생성
        if (employeeTodayCommuteList.isWorkEnd()) {
            CommuteHistory save = repository.save(new CommuteHistory(employee));
            save.startWorking(); // 출근기록 업데이트
        } else {
            //출근 기록이있는경우 퇴근으로
            employeeTodayCommuteList.getWorkingCommuteHistory().endWorking(); //퇴근기록업데이트
        }
    }
}

 

서비스 계층에서 출퇴근정보에서 필요한정보를 가져오려다보니 코드가 너무 커져서 출근기록을 List로 가진 일급컬렉션을 만들어주고 그안에서 필요한 정보를 연산하는 메서드들을 만들어주고 사용했다.

코드를 줄이고 이해하기 쉽게 만드는데에 일급컬렉션이 유용하게 쓰이는것같다. 

EmployeeCommuteList(일급컬렉션)

@Getter
public class EmployeeTodayCommuteList {
    private List<CommuteHistory> commuteHistoryList;

    public EmployeeTodayCommuteList(Employee employee) {
        this.commuteHistoryList = employee.getCommuteHistories().stream()
                .filter(target -> target.getStartTime().toLocalDate().equals(LocalDateTime.now().toLocalDate()))
                .collect(Collectors.toList());
    }

    public Boolean isWorkEnd(){
        //1.직원의 금일 근무정보가있는경우 
        //퇴근을 안한근무정보가(퇴근시간정보==null)있다면 true, 없다면 false
        //commuteHistoryList.stream().noneMatch(CommuteHistory::getIsWorking) 퇴근안한정보가있다면 false를 리턴
        return commuteHistoryList == null || commuteHistoryList.stream().noneMatch(CommuteHistory::getIsWorking);
    }

    public CommuteHistory getWorkingCommuteHistory(){
    //아직 퇴근하지않은 객체를 찾아서리턴
        return commuteHistoryList.stream().filter(CommuteHistory::getIsWorking)
                .findFirst()
                .orElseThrow(IllegalArgumentException::new);
    }
}

서비스계층에서 직원정보만받아서 내부에서 당일 출퇴근정보만 필터링해서 List로 만들도록 생성자를 만들어줬다. 

그 외에 조건문에서 사용할 메서드나 필요한 객체를 리턴할수있게 메서드를 만들어주었다.

Repository

public interface CommuteHistoryRepository extends JpaRepository<CommuteHistory, Long> {
    
}

 

 

사실 명세만보면 다 쉽게만들수있을줄알았는데 이렇게 로직하나로 고민을 할줄은 몰랐다. 

나는 출퇴근시스템을 카드찍는 장면을 떠올리면서 만들어서 카드를 찍는 하나의 행동으로 출퇴근이 다되도록 만들어봤다.

한번찍으면 출근,두번째는 퇴근,세번째는 다시 출근이되도록.

이 방법말고도 특정시간전에는 모두 출근으로찍히게 하고 퇴근시간이후에는 퇴근으로 찍히게할까 라는 생각도해봤지만 

유연근무제나 반차같은 개념도 있어서 유연성이 떨어진다는 생각이들었다. 

이런 비즈니스적인 요구사항도 최대한 생각하면서 만들어보는게 생각보다 재미있는것같다. 

반응형
반응형

1. 직원등록 기능

직원등록 기능

1. 직원이름 ,매니저인지 아닌지여부 ,입사일자 ,생일을 파라미터로 받는다.

 

예외처리

1. 각 파라미터 요소들은 값을 필수로 넣어주어야한다.

 

DTO

@Getter
public class MemberSaveRequest {
    @NotBlank
    private String name;
    @NotNull
    private Boolean isManager;
    @NotNull
    private LocalDate startDate;
    @NotNull
    private LocalDate birthDay;
}

Controller

@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);
}

Entity객체

@Entity
@Getter
@NoArgsConstructor
public class Employee {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @ManyToOne
    @JoinColumn(name = "position_id")
    private Position position; // 직급
    
    private Boolean isManager;//직책

    @ManyToOne
    @JoinColumn(name = "team_id")
    private Team team;

    @OneToMany(mappedBy = "employee")
    private List<CommuteHistory> commuteHistories;

    @OneToMany(mappedBy = "employee")
    private List<AnnualLeaveRegister> annualLeaveRegisters;

    private LocalDate startDate;
    private LocalDate birthDay;

    public Employee(MemberSaveRequest request) {
        this.name = request.getName();
        this.startDate = request.getStartDate();
        this.birthDay = request.getBirthDay();
        this.isManager = request.getIsManager();
    }
}

 

에러핸들러

@RestControllerAdvice
public class commuteServiceException {

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<String> illegalArgumentExceptionHandle(IllegalArgumentException e) {
        return ResponseEntity.badRequest().body("error : " + e.getMessage());
    }
	
    //Valid에서 발생시키는 에러를 처리.
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        Map<String, String> errors = new HashMap<>();
        e.getBindingResult().getAllErrors()
                .forEach(c -> errors.put(((FieldError) c).getField(), c.getDefaultMessage()));
        return ResponseEntity.badRequest().body(errors);
    }

}

빈값을 보내면 에러가 발생

 

 

 

요청을 보낸대로 잘 등록된걸 확인할수있다.

요청에서 이름 , 직책여부, 입사일, 생일 데이터만을 보내줬기때문에

나머지 필드는 null이 되어있다.

이 값들은 따로 지정해줄수있도록 만들어주었다.

//DTO
@Getter
public class MemberPositionSetDto {
    private String employeeName;
    private Long positionId;
}

@Getter
public class MemberTeamSetDto {
    private String employeeName;
    private Integer teamId;
}

//Service
@Transactional
public void setTeam(MemberTeamSetDto request) {
    Employee target = repository.findByName(request.getEmployeeName());
    Team team = teamRepository.findById(Long.valueOf(request.getTeamId()))
            .orElseThrow(IllegalArgumentException::new);
    target.setTeam(team);
}
@Transactional
public void setPosition(MemberPositionSetDto request) {
    Employee employee = repository.findByName(request.getEmployeeName());
    Team team = teamRepository.findById(request.getPositionId())
            .orElseThrow(IllegalArgumentException::new);
    employee.setTeam(team);
}

//Controller
    @PostMapping("/setTeam")
    public void setTeam(@RequestBody MemberTeamSetDto request) {
        service.setTeam(request);
    }

    @PostMapping("/setPosition")
    public void setPosition(@RequestBody MemberPositionSetDto request){service.setPosition(request);}

이 기능도 나중에 프론트가 생긴다면 조회한 직원의 필드값들을 input태그에 하나씩 넣어주었다가

수정해서 다시 요청하도록 처리하면 2개를 하나로합쳐서 직원정보 수정으로 변경할수있을것같다.

2. 직원조회 기능

DTO

@Getter
@Setter
@NoArgsConstructor
public class EmployeeResponse {
    private String name;
    private String teamName;
    private String role;
    private String birthDay;
    private String WorkStartDate;
    public void setRole(Boolean isManager){
        if(isManager){
            this.role = "Manager";
        }else{
            this.role = "MEMBER";
        }
    }
}

 

//Controller
@GetMapping
public List<EmployeeResponse> getEmployee() {
    return service.getEmployee();
}

//Service
public List<EmployeeResponse> getEmployee() {
                return repository.findAll().stream()
                .map(result -> {
                    EmployeeResponse response = new EmployeeResponse();
                    response.setName(result.getName());
                    response.setBirthDay(String.valueOf(result.getBirthDay()));
                    response.setRole(result.getIsManager());
                    response.setTeamName(Optional.ofNullable(result.getTeam()).map(Team::getTeamName).orElse(""));
                    response.setWorkStartDate(String.valueOf(result.getStartDate()));
                    return response;
                }).collect(Collectors.toList());
        
    }

 

처음에는 이렇게 모든 직원객체를 조회해서 DTO객체인 EmployResponse로 매핑해주는 방법으로 해주었다. 

이렇게 하니 서비스계층의 메서드가 너무 길어지는 문제가 생겼다. 

그래서 중간에 List<Employee> 를 받아서 --> List<EmployResponse> 로 매핑할 클래스를 하나 만들어주었다.

@Getter
public class ListEmployeeResponse {
    private final List<EmployeeResponse> employeeResponseList;

    public ListEmployeeResponse(List<Employee> employeeList) {
        this.employeeResponseList = employeeList.stream().map(result -> {
            EmployeeResponse response = new EmployeeResponse();
            response.setName(result.getName());
            response.setBirthDay(String.valueOf(result.getBirthDay()));
            response.setRole(result.getIsManager());
            response.setTeamName(Optional.ofNullable(result.getTeam()).map(Team::getTeamName).orElse(""));
            response.setWorkStartDate(String.valueOf(result.getStartDate()));
            return response;
        }).collect(Collectors.toList());
    }
}

이 객체에서 직원리스트를 받아서 응답리스트로 매핑을한다음 필드로 가지고있게된다. 

이런걸 일급컬렉션이라고 한다고 하는데. 그냥 사용해본거라 정확한 개념은 나중에 찾아봐야할것같다.

이렇게하면 서비스계층의 뚱뚱한메서드를 한줄로 줄일수있게된다.

public List<EmployeeResponse> getEmployee() {
    return new ListEmployeeResponse(repository.findAll()).getEmployeeResponseList();
}

[

    {
        "name": "김동현",
        "teamName": "개발팀",
        "role": "Manager",
        "birthDay": "1999-01-13",
        "workStartDate": "2024-03-02"
    },
    {
        "name": "박민주",
        "teamName": "디자인",
        "role": "Manager",
        "birthDay": "1999-07-02",
        "workStartDate": "2023-02-28"
    },
    {
        "name": "방세원",
        "teamName": "개발팀",
        "role": "Manager",
        "birthDay": "1999-07-02",
        "workStartDate": "2022-01-18"
    },
    {
        "name": "김민지",
        "teamName": "",
        "role": "MEMBER",
        "birthDay": "2000-01-18",
        "workStartDate": "2023-03-19"
    }

]

 

이렇게 응답을 잘 받아왔다. 

 

1단계의 팀처리기능을 끝내고 직원의 처리기능을 마무리했다.

세세한 로직들에서 어떻게 처리해주어야할지 고민이 많았는데 우선은 큰 에러처리없이 값을 잘 넘겨주도록 해주었다. 

이번에도 하다보니 더 공부해봐야할 내용들이 많이보여서 스프링이 정말 기능이 많다고 느꼈다. 

 

알아볼내용: @Valid , 일급컬렉션

반응형
반응형

1. 팀 등록

 

팀을 등록할 api의 명세이다.

요구사항은 간단하지만 막상 구현하려면 이렇게 간단하게만 작성할수는없다.

비즈니스적으로 예외사항에대한 처리를 해줘야하고

파라미터로 들어오는값도 검증이 필요하다면 해주어야한다.

 

팀 등록기능 

1. 팀을 등록할때에는 팀 이름을 필수로 가져야한다.

 

예외처리

1. 동일한 이름의 팀등록은 제외한다.

 

Controller

@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 엔티티 객체에서 기존에 단순히 팀장코드를 가지고있던 필드에 관계설정을 해줘서 직원객체랑 매핑해주었다.

이렇게 해주어야 Jpql에서 join을 사용해서 테이블끼리 조인해주는게 가능했다. 

Jpql문법에 대해서는 따로 공부해본적은없는데 관계설정해주는것만생각하면 

기본 sql문과 크게 차이없이 사용이가능한것같다.

 

이렇게 미니프로젝트 팀관련 api는 모두 처리해주었다. 

다음은 직원에 관련된 api기능을 처리해주자.

 

반응형
반응형

7일동안 과제를 마치고 이제 미니프로젝트 과제를 하게되었다. api 명세를 받아서 

api를 개발하면 되는데 지금 다 끝난시점에서 쓰는거지만 하나씩 할때마다 테이블에대해서도 고민하고 쿼리에대해서도 여러가지 생각을 해보게되는 좋은 경험이었다. 

 

[ 프로젝트 1단계 ]

SpringInitilizer로 스프링프로젝트를 생성한후에 실행해주었다.

 

웹으로 접근이 가능하게 하기위해서 spring web

Jpa를 쓰기위해서 spring Jpa

MySql 데이터베이스를 사용하기위해서 MySQL Driver

코드를 줄이고 편하게작성하기위해서 lombok을 의존성에 추가해서 프로젝트를 만들어주었다.

Dependencies추가

 

 

이제 필요한 기능명세에 따라서 기능을 만들어주겠다.

 

위의 기능을 보고 컨트롤러를 작성하기전에 DB를 먼저 생성해주었다. 

필요한 정보를보면 직원의정보 , 직원이 속한 부서 ,직원의 직급이 필요하다.

 

직원의 정보에 부서와,직급정보가 모두 포함될수도있지만 중복이 발생하기때문에 서로 분리해주었다.(정규화)

 

[직원]

직원코드, 직원 이름 , 직급(코드) , 부서(코드) , 생일 , 입사일

 

[직급]

직급코드, 직급이름

 

[부서]

부서코드, 부서이름 , 부서장(직원코드)

 

이렇게 간단하게 개념을 정리하고 테이블을 설계해주었다 코드는 기본키로설정하고

auto Increase로 자동증가하도록 해주었다. 

 

그리고 이런개념을 정리하기위해서 erd를 작성해보았다.

 

아래로가는 화살표는 이후 과제를 하면서 추가되는 테이블이다.

직급과 부서에는 해당하는 정보만 저장되도록하고 직원테이블에는 직급과 부서를 외래키로 참조하도록해서

직급의 이름과 부서의 이름을 받아올수있도록 해주었다. 

(사실 이게 제대로 정규화가 된건지는 잘모르겠다.)

이어진 선을보면 알수있는데 직급,부서 하나에 직원이 여러명 연결된 1:N관계이다.

 

이제 이 구조대로 자바코드에 DAO객체를 만들어주고 JpaRepository를 만들어주면 api를 만들 준비가 끝난다.

 

Employee(DAO)

@Entity
@Getter
@NoArgsConstructor
public class Employee {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @ManyToOne
    @JoinColumn(name = "position_id")
    private Position position;

    @ManyToOne
    @JoinColumn(name = "team_id")
    private Team team;

    private String startDate;
    private String birthDay;
}

 

@NoArgsConstructor어노테이션을 사용하면 파라미터가없는 기본생성자가 생성된다.( lombok )

EmployeeRepository(Repository)

public interface EmployeeRepository extends JpaRepository<Employee,Long> {
    Employee findByName(String name);
}

 

직원의 이름으로 값을 받아올수있는 추상메서드를 하나 작성해두었다.

 

Position(DAO)

@Entity
@NoArgsConstructor
@Getter
public class Position {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long positionId;
    private String positionName;
    @OneToMany(mappedBy = "position")
    private List<Employee> employeeList;
}

 

 

직급은 여러개의 직원정보를 가질수가있다.  (1:N)

예를 들어 직급이 대리 인데이터를 직원테이블에서조회한다고하면 김대리,이대리,박대리 가 나올수있는것이다.

그래서 직급하나가 여러개의 직원정보를 가져서 1:N관계가 되는것이다.

Jpa에서는 이런관계를 @OneToMany로풀어낼수있다.

이 기능을 이용하기위해서는 직급코드를 가지고있는

Employee클래스에서도 @ManyToOne으로 표기를 해주어야 서로 매핑이 되어서 이용이 가능하다. 

 

이 관계에는 주인이있는데 상대방을 알고있는쪽이 주인이라고 생각하면 편하다 

직원 클래스는 직급의코드를 가지고있어서 직급에대한 정보를 알고있지만 

직급 클래스는 직원에대해서 어떠한 정보도 가지고있지않다. 

 

그래서 직급클래스에는 mappedBy속성으로 관계의 주인을 명시해주고

직원 클래스에는 @JoinColumn을 사용해서 외래키인 position_id를 명시해주어야한다. - (쿼리에 들어갈 컬럼이름)

PositionRepository(Repository)

public interface PositionRepository extends JpaRepository<Position,Long> {
    Position findByPositionName(String positionName);
}

직급이름으로 조회할수있게 추상메서드를 작성해주었다.

 

Team(DAO)

@Entity
@Getter
@NoArgsConstructor
public class Team {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long teamId;
    private String teamName;
    private Integer teamLeaderId;
    @OneToMany(mappedBy = "team")
    private List<Employee> employeeList;
}

 

TeamRepository(Repository)

public interface TeamRepository extends JpaRepository<Team,Long> {
    Team findByTeamName(String teamName);
}

부서이름으로 조회할수있게 추상메서드를 작성해주었다.

 

 

 

\main\resources\application.yml

spring:
  datasource:
    url: "jdbc:mysql://localhost:3308/company"
    username: "root"
    password: ""
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    hibernate:
      ddl-auto: none
    properties:
      hibernate:
        show_sql: true
        format_sql: true
        dialect: org.hibernate.dialect.MySQL8Dialect

리소스에 applocation으로 설정정보를 작성까지 해주었다.

 

이제 데이터베이스에 접근하기위한 준비가 모두 끝났다. 이제 API명세를 보고 필요한 기능을 하나씩 작성해보자.

반응형
반응형

 

FruitService

(기존코드)

@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);
}

 

Interface로 repository를 만들어주고 JpaRepository를 상속받아준다. 

Jpa의 기본기능은 따로 상속받아서 오버라이딩 하지않아도.

save나 findById findAll같은 기본조회나 업데이트가 사용가능하다.

 

Service

@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에서는 쿼리문을 만들지않아도되기때문에 간단하게 추상 메서드를 두개 만들어서 사용해주었다.

 

repositoryJpa

List<Fruit> findByPriceGreaterThan(Integer price);
List<Fruit> findByPriceLessThan(Integer price);

 

Service

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에서 문자열로들어오는조건에 따라서 다른 메서드를 실행하게 처리해서 적용해주었다. 

 

Controller

@GetMapping("/list")
public List<FruitResponse> getListCondition(String condition, Integer price){
    return service.getListCondition(condition,price);
}

 

 

Jpa를 새로 적용하면서 중간에 에러도 많이 발생했는데 거의 대부분이

객체의 필드와 조회된 테이블이 매칭되지않아서 생기는 문제였다. 

이후에 익숙해지고 컨버터 개념도 배우면 좀 쉽게 해결이가능할것같지만 우선은 지금사용하는정도로도 

코드의 양을 눈에 띄게 줄일수있고 보는걸로 db에서 어떤기능을 하는지 쉽게 파악할수있게되었다.

반응형
반응형

4일차에 만든 api를 분리해서 리팩토링 해야한다. 

@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 interface FruitRepository {
    void saveFruit(String name,String date,Long price);

    void updateFruit(long id);

    boolean isFruitExist(long id);
    FruitShopAmoutResponse getFruitStat(String 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);
}

 

이렇게 응답객체와 요청객체를 사용해서 컨트롤러를 만들었다. 

 

요청에 따른 응답이 적절하게 잘 오고있다.

 

여기서는 LocalDate클래스 를 이용해서 만들었다.

해당 객체에대해 아는게 없어서 조금 헤매기는 했지만 간단하게 구현할수있었다.

 

@GetMapping("/api/v1/dayOfWeek")
public String calcDate(String date){
    int[] dateArray = parseDate(date);
    LocalDate localDate = LocalDate.of(dateArray[0],dateArray[1],dateArray[2]);
    return localDate.getDayOfWeek().toString();
}

private int[] parseDate(String dateString) {
    return Arrays.stream(dateString.split("-"))
            .mapToInt(Integer::parseInt)
            .toArray();
}

 

LocalDate 객체는 생성자가 private으로 되어있어서 new LocalDate( ) 이런방법으로는 객체를 생성해서 날짜 파라미터를 넣는게 불가능했다. 그래서 찾아보니 .of라는 메서드를 통해서 객체에 파라미터를 넣어줄수있어서 이 방법을 사용했다.

 

아래의 메서드는 날짜데이터를 String으로 받아서 구분자로 연 , 월 ,일  을 구분해서 배열에 넣어주는 메서드이다. 

이렇게 하면 LocalDate에 날짜파라미터를 넣고 메서드를통해서 요일을 쉽게 출력해줄수있다.

실제  23년1월1일은 일요일 이었따..

 

여기서는 좀 시행착오가있었다. 

@PostMapping("/api/v1/arraySum")
public Integer arraySum(@RequestBody List<Integer> numbers){
    return numbers.getNumbers().stream().mapToInt(Integer::intValue).sum();
}

처음에는 객체를 따로 생성하지않기위해서 List<Integer> 를 파라미터로 넣어서 시도해봤는데 에러가 발생했다.

 

그래서 따로 요청 객체를 생성해주었다.

public class CalculatorArrayRequest {
    private final List<Integer> numbers = new ArrayList<>();

    public List<Integer> getNumbers() {
        return numbers;
    }
}

해당 리스트를 필드로 가지는 객체를생성하고 그 객체를 받아오기위해서 getter메서드만 넣어주었다.

//객체로 파라미터를 받도록 변경
@PostMapping("/api/v1/arraySum")
public Integer arraySum(@RequestBody CalculatorArrayRequest numbers){
    return numbers.getNumbers().stream().mapToInt(Integer::intValue).sum();
}

 

 

찾아보니 Json으로 오는값을 받기위한 객체가 필요하고 객체없이 쓰려면

HttpEntity라는 클래스를 이용해서 Json으로 오는 값을 받아야하는데 그 값을 받은후에

파싱하고 연산하는 과정을 생각하면 객체를 만들어서 그 과정을 자동화 시켜주는게 훨씬 이득인것같았다 .. 

 

스프링에서 자체적으로 해주는 자동화 기능의 혜택을 받기위해서는 지켜야할게 많지만

지켰을때에 득이 크다는 생각이 들었다 

반응형
반응형

[질문]

  • 어노테이션을 사용하는 이유 (효과) 는 무엇일까?
  • 나만의 어노테이션은 어떻게 만들 수 있을까?

 

자바에는 3종류의 어노테이션 이 있다.

 

표준 어노테이션 - 자바에서 기본적으로 제공하는 기능인 어노테이션

메타 어노테이션 - 어노테이션을 만들기위한 어노테이션

사용자정의 어노테이션 - 사용자가 직접 생성한 어노테이션

 

이런 어노테이션들은 주석처럼 사용자가 읽었을때 의미를전달할수도있지만

프로그램이 이 어노테이션을 읽어서 특정 어노테이션 마다 다른 처리를 하도록 할수도있다.

 

어노테이션을 찍었을때 해당 메서드나 클래스에 로그를 찍도록 처리할수도있고 특정과정을 추가할수도 없앨수도 있다.

이런 점에서는 코드에 마킹을 해놓는 기능이라고 생각한다. 

반응형

+ Recent posts