반응형

4일차 과제는 이 코드를 리팩토링하고

Solid에 대해서 설명하는 내용이다.

우선은 리팩토링부터해보자

리팩토링

public boolean validateOrder(Order order) {
    if (order.getItems().size() == 0) {
        log.info("주문 항목이 없습니다.");
        return false;
    } else {
        if (order.getTotalPrice() > 0) {
            if (!order.hasCustomerInfo()) {
                log.info("사용자 정보가 없습니다.");
                return false;
            } else {
                return true;
            }
        } else if (!(order.getTotalPrice() > 0)) {
            log.info("올바르지 않은 총 가격입니다.");
            return false;
        }
    }
    return true;
}

 

1. if- else 제거

public boolean validateOrder(Order order) {
    if (order.getItems().size() == 0) {
        log.info("주문 항목이 없습니다.");
        return false;
    }
    if (order.getTotalPrice() > 0) {
        if (!order.hasCustomerInfo()) {
            log.info("사용자 정보가 없습니다.");
            return false;
        }
            return true;
    }
    if (!(order.getTotalPrice() > 0)) {
        log.info("올바르지 않은 총 가격입니다.");
        return false;
    }
    return true;
}


위 코드에서 if - else 문을 모두 if 문으로 변경해줬다.

이미 코드내에서 조건문에 해당하면 return으로 반환하면서 함수를 끝내기때문에 else로 분기문을 나눌필요가 없었다.

  • 조건1 - 주문아이템이 0개이면 false
  • 조건2 - 주문한상품금액이 0  이상인 경우
    • 조건2-1 - 주문에 사용자정보가없는경우 false
    • 조건2-2 - 사용자정보가있으면 true를 리턴
  • 조건3 - 주문금액이 0원보다적은경우 false

조건을 정리하다보니 기본 리턴을 true로 잡고 각 예외상황에 대한 처리만 조건으로 해주면 될것같다. 

 

 

부정연산자 제거

public boolean validateOrder(Order order) {
    if (order.getItems().size() == 0) {
        log.info("주문 항목이 없습니다.");
        return false;
    }
    if (order.getTotalPrice() > 0) {
        if (orderHasNotCustomerInfo(order)) {
            log.info("사용자 정보가 없습니다.");
            return false;
        }
    }
    if (order.getTotalPrice() < 0) {
        log.info("올바르지 않은 총 가격입니다.");
        return false;
    }
    return true;
}

private boolean orderHasNotCustomerInfo(Order order) {
    return !order.hasCustomerInfo();
}

 

! 연산자로 한번 뒤집어서 생각하게되던 연산자를 제거하고 메서드로 추출해서 코드의 의미를 좀 더 빠르고 명확하게 이해할수있게만들었다. 

 

  • !(order.getTotalPrice( ) > 0) 부분은 - totalPrice가 0이상이 아닌경우 인데 부등호를 뒤집어서 부정연산자를 빼주었다 
  • !order.hasCustomerInfo( ) 는 order가 고객정보를 가졌는지를 boolean으로 리턴하는 메서드인것같은데 우선은 여기서 메서드로 추출해서 부정을하는 메서드를 만들었다. 실제 부분에서는 해당 Order클래스에 고객정보가없을때 true를 리턴해주는 메서드를 만드는것도 좋을것같다.

메서드로 추출해서 추상화레벨 올리기

    public boolean validateOrder(Order order) {
        if (order.doesNotHaveItem()) {
            log.info("주문 항목이 없습니다.");
            return false;
        }

        if (totalPriceBiggerThanZero(order)) { //토탈주문금액이 0원이 아닌데 사용자정보가없는경우 - 정상주문인데 사용자 정보가없는경우
            if (orderHasNotCustomerInfo(order)) {
                log.info("사용자 정보가 없습니다.");
                return false;
            }
        }

        if (totalPriceLowerThanZero(order)) {
            log.info("올바르지 않은 총 가격입니다.");
            return false;
        }
        return true;
    }

    private static boolean totalPriceLowerThanZero(Order order) {
        return order.getTotalPrice() < 0;
    }

    private static boolean totalPriceBiggerThanZero(Order order) {
        return order.getTotalPrice() > 0;
    }

    private boolean orderHasNotCustomerInfo(Order order) {
        return !order.hasCustomerInfo();
    }

 

이번에는 조건문에들어가는 조건들의 추상화 레벨을 올려주었다.

함수의 이름만으로 어떤걸 검사해서 어떤 값을 리턴하는지 파악이 가능해졌다. 

이제 중첩된 조건문을 수정하려고하는데 2가지 조건으로 검사한후 false를 리턴해주고있다

 

1. 토탈주문금액이 0원이 아닌경우

2. 주문한사용자의 정보가 없는경우

개인 적인 생각이지만 주문한 사용자의 정보가없는경우는 무조건 false가 나와야하지않을까 라는 생각이들었다. 

 

case1 - 주문을 한 금액은 있는데 주문자 정보가없는경우

case2 - 주문을 한 금액은 있는데 주문자 정보가 있는경우 ok

 - 주문한 금액이없는경우는 아래의 totalPriceLowerThanZero에서 걸러진다.

 

케이스를 봤을때 주문한금액으로 검증할 필요가 없을것같다 totalPriceLowerThanZero를 위로올리면 주문금액이 0원 이하인 케이스는 모두 걸러지고 아래에는 토탈주문금액이 0원이상인 케이스만 남아있게된다.

 

조건문의 순서를 변경해서 중첩된 조건문제거

public boolean validateOrder(Order order) {
        if (order.doesNotHaveItem()) {
            log.info("주문 항목이 없습니다.");
            return false;
        }

        if (totalPriceLowerThanZero(order)) {
            log.info("올바르지 않은 총 가격입니다.");
            return false;
        }

        if (orderHasNotCustomerInfo(order)) {
            log.info("사용자 정보가 없습니다.");
            return false;
        }
        return true;
    }

    private static boolean totalPriceLowerThanZero(Order order) {
        return order.getTotalPrice() < 0;
    }

    private static boolean totalPriceBiggerThanZero(Order order) {
        return order.getTotalPrice() > 0;
    }

    private boolean orderHasNotCustomerInfo(Order order) {
        return !order.hasCustomerInfo();
    }

 

최종적으로 이렇게 리팩토링 해주었다. 

각 조건문마다 검증하는 항목이 달라서 메소드로 추출할까 고민했는데.

메서드로 추출해도 각 항목에서 early return 으로 메서드를 종료하려면 조건문을 똑같이 3개써야 할것같아서.

현재는 이정도가 딱 좋다고 생각한다.

 

SOLID에 대한 정리

  • SRP - single responsibility principle 단일 책임 원칙
    • 하나의 클래스는 하나의 책임만 가진다.
    • 클래스를 생성하고 기능을 정의하면서 클래스가 가진 역할이 여러개인경우 클래스를 분리한다.
    • Money라는 클래스는 돈에대한 역할만 수행하고 돈을 저장하거나 계산하는 역할은 다른 클래스로 분리가가능하다. 돈을 저장하는건 지갑이나 , 창고같은 클래스에 맡기고 계산은 계산기가 한다.
  • OCP - Open-Closed principle
    • 확장에 열려있고 수정에는 닫혀있다.
    • 기능을 쉽게 확장하고 적게 수정
    • 인터페이스나 추상화를통해서 필수기능들을 정의하고 구현체로 기능을 구현 , 기능에대해 수정사항이나 각 상황별 다양한 구현이 필요하다면 그때에 각각 다른 구현체들로 변경하면서 기능을 확장할수있다.
  • LSP - Liskov Substitution Principle
    • 부모클래스의 인스턴스를 자식클래스의 인스턴스로 치환할수 있어야 한다.
    • 부모클래스의 기능을 확장한 자식클래스가 들어가도 동일하게 동작해야한다.
  • ISP - Interface Segregation Principle
    • 사용하지않는 인터페이스에 의존하면 안된다.
    • 만약 사용하지않는 인터페이스에 의존하게된다면 해당 인터페이스의 기능이 충분히 분리되지않았다. 더 작게 기능을 분리해서 필요한 인터페이스에만 의존하도록한다.
  • DIP - Dependency Inversion Principle
    • 상위수준모듈은 저수준모듈에 의존해서는 안된다. 둘다 추상화에 의존
    • 저수준 모듈에 의존하게된다면 저수준 모듈이 수정될때 고수준 모듈도 같이 수정해야한다.
    • 추상화에 의존하게된다면 구현부분인 저수준모듈이 수정되어도 고수준모듈은 수정되지않는다




반응형
반응형

 

 

요청할때 필요한 데이터는 연장근무를 찾아올 연도와 월 , 그리고 근무하지않는일수를 파라미터로 받도록 해주었다.

 

OverTimeRequest

@Getter
@Setter
@Builder
public class OverTimeRequest {
    @NotBlank
    @DateTimeFormat(pattern = "yyyy-MM")
    private YearMonth yearMonth;
    @NotNull
    private Integer holidays;
}

 

보통은 리퀘스트로 들어오는 클래스는 직접생성할일은 없기때문에 빌더를 붙이지는 않는데 이후에 추가될 기능때문에 

해당 클래스를 파라미터로 넣어줘야할 일이생겨서 빌더를 붙여주게되었다. 

 

이후에는 리팩토링을해서 request는 컨트롤러로 생성만되도록하고

추가적으로 사용하는메서드는 다른 파라미터를 사용하도록 변경해줘야한다.

 

MemberController

@PostMapping("/overTimeCalc")
public List<OverTimeResponse> getOverTime(@Validated @RequestBody OverTimeRequest overTimeRequest) {
    return service.getOverTime(overTimeRequest);
}

 

기존에 작성되어있던 MemberController에 추가해주었다.

직원의 연장근무정보를 조회하는 기능이기때문에 직원컨트롤러에서 접근하는게 좋겠다는 생각이었다.

 

MemberService

@Transactional
public List<OverTimeResponse> getOverTime(OverTimeRequest request) {
    YearMonth yearMonth = request.getYearMonth();
    Integer holidays = request.getHolidays();

    //근무규정의 일 근무시간을 조회
    CompanyPolicy policy = policyRepository.findByPolicyGubnAndPolicyName("workTime", "dailyWorkLimit")
            .orElseThrow(() -> new IllegalArgumentException("없는 정책입니다."));

    //해당하는 달의 총 평균근무시간을 계산 근무일 * 근무시간
    int standardWorkHour = calcStandardWorkDays(yearMonth, holidays) * Integer.parseInt(policy.getPolicyContent());

    List<Employee> employeeList = repository.findAll();
    OverTimeResponseList overTimeResponseList = new OverTimeResponseList(employeeList, yearMonth, standardWorkHour);
    return overTimeResponseList.getOverTimeResponseList();
}

 

1. 근무규정 테이블에서 표준근로시간을 조회

2. 해당달의 평균근무시간을 계산 (해당월의 총일수 - 휴무일) * 일 표준근로시간 = 월 근무시간

3. 직원정보를 모두 조회

4. OverTimeResponseList (overTimeResponse를 리스트로가진 래퍼클래스) 생성

5. List<OverTimeResponse>를 리턴

 

OverTimeResponseList (래퍼클래스)

@Getter
@AllArgsConstructor
public class OverTimeResponseList {
    private List<OverTimeResponse> overTimeResponseList;

    public OverTimeResponseList(List<Employee> employeeList,YearMonth yearMonth,Integer standardWorkHour) {
        this.overTimeResponseList = employeeList.stream().map(result -> {
            Integer employeeWorkHour = new EmployeeCommuteList(result).filterByYearMonth(yearMonth).calcEmployeeWorkHour();
            return OverTimeResponse.builder()
                    .id(Math.toIntExact(result.getId()))
                    .name(result.getName())
                    .overtimeMinutes(getOverTime(employeeWorkHour, standardWorkHour))
                    .build();
        }).collect(Collectors.toList());
    }

    private Integer getOverTime(Integer workTime, Integer standardWorkHour) {
        Integer standardWorkMinute = standardWorkHour * 60;
        if (workTime > standardWorkMinute) {
            return workTime - standardWorkMinute;
        } else {
            return 0;
        }
    }
}

 

직원의 근무시간정보는 직원이가지고있는 commuteHistory를

일급컬렉션인 EmployeeCommuteList에 넣고 거기서 연월로 필터링해준후

근무시간을 더해주는 메서드를 구현해서 구해주었다. 

id , name ,근무시간 정보를 overTimeResponse에 넣어준후 List로 반환 해주면 

OverTimeResponseList의 필드인 overTimeResponseList에 값이 세팅된다.

 

http://localhost:8080/member/overTimeCalc?yearMonth=2024-03&holidays=31

 

조회 결과

근무 일자를 0일로 만들기위해서 휴무일을 31일을 넣어주었다.

그러면 표준근로시간은 0시간이되고 직원의 근무시간이 나오게된다.

현재 가지고있는데이터에서는 1번아이디 직원외에는

1분을넘는 근무시간을 가진 직원이없기때문에 나머지는 0으로 잘나오고있는것같다.

반응형
반응형

연차 정보가 생기면서 추가된 요구사항.

 

직원이 연차를 신청할수있게되면서 직원의 근무시간을 조회하는 기능에 추가적인 요구사항이 생겼다. 

근무시간을 조회하면서  연차를 사용한날도 같이 조회해주어야한다. 

 

기존에 직원의 정보를 조회해오던 코드의 구조

Controller

@GetMapping("/worktime")
public CommuteTotalResponse checkTime(
        @Validated
        CommuteGetDto request) {
    return service.checkTime(request);
}

Service

@Transactional(readOnly = true)
public CommuteTotalResponse checkTime(CommuteGetDto request) {
	//직원아이디로 직원을 조회
    Employee employee = memberRepository.findById(request.getEmployeeId())
            .orElseThrow(()-> new IllegalArgumentException("직원 정보가없습니다."));
    //직원이 가진출퇴근 정보와 검색정보로 응답객체인 CommuteTimeResponseList를 생성
    CommuteTimeResponseList commuteTimeResponseList = new CommuteTimeResponseList(request, employee);

    return new CommuteTotalResponse(commuteTimeResponseList.getCommuteTimeResponseList());
}

 

CommuteTimeResponseList

@Getter
public class CommuteTimeResponseList {
    List<CommuteTimeResponse> commuteTimeResponseList;

    public CommuteTimeResponseList(CommuteGetDto request, Employee employee) {
        List<CommuteTimeResponse> collect = filterAndGroupCommuteHistories(request, employee);

        //연차 정보를 더해준다. //추가된 메서드
        addAnnualLeaveToCommuteTimeResponse(request, employee, collect);

        //정렬
        this.commuteTimeResponseList = sortCommuteTimeResponseListbyDate(collect);
    }

    private static List<CommuteTimeResponse> sortCommuteTimeResponseListbyDate(List<CommuteTimeResponse> collect) {
        List<CommuteTimeResponse> list = collect.stream()
                .sorted(Comparator.comparing(response -> LocalDate.parse(response.getDate())))
                .toList();
        return list;
    }

    private static void addAnnualLeaveToCommuteTimeResponse(CommuteGetDto request, Employee employee, List<CommuteTimeResponse> collect) {
        employee.getAnnualLeaveRegisters().stream()
                .filter(leave -> {
                    YearMonth startYearMonth = YearMonth.from(leave.getStartDate());
                    YearMonth endYearMonth = YearMonth.from(leave.getEndDate());
                    return !startYearMonth.isAfter(request.getSearchDate()) && !endYearMonth.isBefore(request.getSearchDate());

                })
                .forEach(leave -> {
                    LocalDate startDate = leave.getStartDate();
                    LocalDate endDate = leave.getEndDate();

                    // 검색 시작일과 휴가 시작일 중 늦은 날짜를 선택
                    LocalDate searchStart = request.getSearchDate().atDay(1);
                    LocalDate effectiveStartDate = startDate.isBefore(searchStart) ? searchStart : startDate;

                    // 검색 종료일과 휴가 종료일 중 이른 날짜를 선택
                    LocalDate searchEnd = request.getSearchDate().atEndOfMonth();
                    LocalDate effectiveEndDate = endDate.isAfter(searchEnd) ? searchEnd : endDate;

                    long daysBetween = ChronoUnit.DAYS.between(effectiveStartDate, effectiveEndDate);
                    for (long i = 0; i <= daysBetween; i++) {
                        LocalDate currentDate = leave.getStartDate().plusDays(i);
                        collect.add(new CommuteTimeResponse(currentDate));
                    }
                });
    }

    private List<CommuteTimeResponse> filterAndGroupCommuteHistories(CommuteGetDto request, Employee employee) {
        return employee.getCommuteHistories().stream()
                //연월 필터링
                .filter(result ->
                        result.isSameYearMonth(request.getSearchDate())
                                && !result.getIsWorking()
                )
                //그루핑
                .collect(Collectors.groupingBy(
                        result -> dateTimeToFormatString(result.getStartTime(), "yyyy-MM-dd"),
                        Collectors.summingInt(result -> diffMinute(result.getStartTime(), result.getEndTime()))
                ))
                .entrySet().stream()
                .map(result -> {
                    CommuteTimeResponse response = new CommuteTimeResponse();
                    response.setDate(result.getKey());
                    response.setWorkingMinutes(result.getValue());
                    return response;
                }).collect(Collectors.toList());
    }

    private String dateTimeToFormatString(LocalDateTime time, String format) {
        return time.format(DateTimeFormatter.ofPattern(format));
    }

    private Integer diffMinute(LocalDateTime start, LocalDateTime end) {
        return (int) Duration.between(start, end).toMinutes();
    }
}

 

코드를 이해하기쉽게 쓰려고 풀어서 쓰다보니까 상당히 길어졌는데.

기존에 날짜로 그룹화시켜서 리스트로매핑했던데이터에 휴가데이터를 추가해주는 메서드가 추가되었다.

휴가인 날은 usingDayOff 가 true인것이 고정이기때문에

생성시에 고정으로 근무시간은 0 DayOff는 true가 들어간 데이터가 리스트에 추가된다.

그후에 날짜값으로 데이터를 정렬해주게된다.

 

지금은 우선 기능을구현하려고 한 클래스에 넣어놨지만 코드가 너무길어져서 따로 클래스를 더 만들어야 할것같다.

나눈다면 아마 public에서 사용하고있는 3개의 메서드들을 따로 클래스로 만들어서 관리하게될것같다.

 


리팩토링

package Company.demo.commute.dto.firstCollection.commutetimeresponse;

import Company.demo.commute.dao.AnnualLeaveRegister;
import Company.demo.commute.dao.CommuteHistory;
import Company.demo.commute.dao.Employee;
import Company.demo.commute.dto.firstCollection.annualleavelist.AnnualLeaveToCommuteResponse;
import Company.demo.commute.dto.firstCollection.annualleavelist.FilterAnnualLeaveRegistered;
import Company.demo.commute.dto.firstCollection.commutehistorylist.FilterCommuteHistories;
import Company.demo.commute.dto.firstCollection.commutehistorylist.GroupCommuteHistories;
import Company.demo.commute.dto.request.CommuteGetDto;
import Company.demo.commute.dto.response.CommuteTimeResponse;
import java.time.LocalDate;
import java.time.YearMonth;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import lombok.Getter;

@Getter
public class CommuteTimeResponseList {
    List<CommuteTimeResponse> commuteTimeResponseList;

    public CommuteTimeResponseList(CommuteGetDto request, Employee employee) {
        List<CommuteHistory> filtered = new FilterCommuteHistories(employee.getCommuteHistories())
                .filterByDate(request.getSearchDate());
        Map<String, Integer> grouped = new GroupCommuteHistories(filtered).groupByDate();
        List<CommuteTimeResponse> commuteTimeResponsesByCommuteHistories = mapToCommuteTimeResponse(grouped);

        //연차 정보를 더해준다.
        List<CommuteTimeResponse> commuteTimeResponsesByAnnualLeave = AnnualLeaveToCommuteTimeResponse(request.getSearchDate(), employee);
        commuteTimeResponsesByCommuteHistories.addAll(commuteTimeResponsesByAnnualLeave);

        //정렬
        this.commuteTimeResponseList = new SortCommuteTimeResponse(commuteTimeResponsesByCommuteHistories).sort();
    }

    private static List<CommuteTimeResponse> AnnualLeaveToCommuteTimeResponse(YearMonth searchYearMonth, Employee employee) {
        List<AnnualLeaveRegister> filter = new FilterAnnualLeaveRegistered(employee.getAnnualLeaveRegisters()).filter(searchYearMonth);
        return new AnnualLeaveToCommuteResponse(filter).leaveToResponse(searchYearMonth);
    }

    private List<CommuteTimeResponse> mapToCommuteTimeResponse(Map<String, Integer> groupedCommuteHistory) {
        return groupedCommuteHistory.entrySet().stream()
                .map(result -> {
                    CommuteTimeResponse response = new CommuteTimeResponse();
                    response.setDate(result.getKey());
                    response.setWorkingMinutes(result.getValue());
                    return response;
                }).collect(Collectors.toList());
    }
}

 

각 메소드들을 기능별로 분리하고 정렬 그루핑 매핑 각각 일급컬렉션으로 분리해서 만들어줬다. 

반응형
반응형

연차신청 기능
연차 조회기능

 

현재의 데이터베이스는 직원의 연차에 대해 저장하는 컬럼이 아무데도 없으므로 데이터베이스 부터 수정해주었다.

서비스 테이블구조

직원테이블에 직원이 몇개의 연차를 가지고있는지를 저장하기위해서 연차 컬럼을 추가해주었고.

신청한연차를 기록하기위해서 연차기록테이블을 만들어주었다. 

올해입사한 직원은 11일 그외에는 15일 이라는 규정을 상수로 만들까하다가 변경이 생기는경우

데이터베이스에서 모두 처리해주기 위해서 정책테이블을 만들어주었다. 

연차에대한 규정은 annualLeave(정책구분) - newEmployee , experienced (정책이름) 으로 조회해서 정책내용으로 받아올수있다. 나름 트리 구조나 카테고리같은 구조를 생각하고 만들었는데 효용성이 있는지는 잘 모르겠다.

나중에 정책구분도 따로 테이블이 생긴다면 외래키로 조회해서

연차관련 정책을 한번에 받아온다던가 하는데에 쓰일것같아서 만들어봤다.

 

연차세팅

우선은 직원들의 연차를 초기화 시킬수있는 기능을 만들었다.

 

Controller

@RestController
@RequiredArgsConstructor
@RequestMapping("/annual-leave")
public class AnnualLeaveController {

    private final AnnualLeaveService service;

    @PutMapping("/set-annual-leave")
    public void setAnnualLeave(){
        service.setAnnualLeave();
    }
}

 

Service

@Service
@RequiredArgsConstructor
public class AnnualLeaveService {
    private final MemberRepository memberRepository;
    private final CompanyPolicyRepository policyRepository;

    @Transactional
    public void setAnnualLeave() {
        int newEmployee = getAnnualLeavePolicy("newEmployee");
        int experienced = getAnnualLeavePolicy("experienced");
        memberRepository.findAll().stream().forEach(employee -> {
            //올해
            int thisYear = LocalDate.now().getYear();
            //직원의 입사연도정보
            int employeeStartYear = employee.getStartDate().getYear();
            if(employeeStartYear == thisYear){
                employee.setAnnualLeave(newEmployee);
            }else{
                employee.setAnnualLeave(experienced);
            }
        });
    }

    private int getAnnualLeavePolicy(String employeeStatus){
        String policyGubn = "annualLeave";
        return Integer.parseInt(policyRepository.findByPolicyGubnAndPolicyName(policyGubn,employeeStatus)
                .orElseThrow(()->new IllegalArgumentException("없는 정책입니다."))
                .getPolicyContent());
    }
}

Repository

public interface MemberRepository extends JpaRepository<Employee,Long> {
    Employee findByName(String name);
}
public interface CompanyPolicyRepository extends JpaRepository<CompanyPolicy,Long> {
    Optional<CompanyPolicy> findByPolicyGubnAndPolicyName(String policyGubn, String policyName);
}

 

이미 만들어서사용했던 MemberRepository를 사용해서 직원데이터를 조회하는데 사용하고

정책 테이블이 새로생겨서 정책을 조회할수있는 CompanyPolicyRepository를 만들어주었다. 

그리고 정책내용을 정책구분과,정책이름으로 조회할수있게 추상메서드를 만들어줬다.

 

해당 컨트롤러로 요청을보내면 직원데이터의 입사일을 보고 신입은 11일 그외에는 15일로 세팅이된다.

 

연차신청 

Controller

@RestController
@RequiredArgsConstructor
@RequestMapping("/annual-leave")
public class AnnualLeaveController {

    private final AnnualLeaveService service;

    @PutMapping("/set-annual-leave")
    public void setAnnualLeave(){
        service.setAnnualLeave();
    }

    @PostMapping("/register")
    public void registerAL(@Valid @RequestBody ALRequest request) {
        service.registerAl(request);
    }
}

DTO

@Data
public class ALRequest {
    private Long employeeId;
    @FutureOrPresent // 현재거나이후의 시간이어야한다.
    private LocalDate startDate;
    @FutureOrPresent
    private LocalDate endDate;

    @AssertTrue(message = "endDate는 startDate보다 이후여야 합니다.") // endDate는 startDate의 이어야한다.
    public boolean isDateCheck(){
        if(startDate.isBefore(endDate)){
            return true;
        }
        return false;
    }
}

 

DTO에는 검증을하기위해서 어노테이션과 검증용 메서드를 만들었다.

이걸로 파라미터로 들어오는 값들을 간단하게 검증하고 해당조건을 만족하지못하는경우 예외를 발생시킨다.

Service

@Service
@RequiredArgsConstructor
public class AnnualLeaveService {
    private final MemberRepository memberRepository;
    private final AnnualLeaveRepository annualLeaveRepository;
    private final CompanyPolicyRepository policyRepository;

    @Transactional
    public void registerAl(ALRequest request) {
        LocalDate startDate = request.getStartDate();
        LocalDate endDate = request.getEndDate();
        Employee employee = memberRepository.findById(request.getEmployeeId())
                .orElseThrow(() -> new IllegalArgumentException("직원 정보가없습니다. "));
        //팀 휴가 사전보고일을 조회해서 사전보고일보다 늦게 보고시 에러처리
        Integer annualLeaveBefore = employee.getTeam().getAnnualLeaveBefore();
        long between = ChronoUnit.DAYS.between(LocalDate.now(), startDate);
        if (between < 0 || between < annualLeaveBefore) {
            throw new IllegalArgumentException(String.format("휴가 신청이 불가능합니다. %d 일 전에 팀에 보고해야 합니다.", annualLeaveBefore));
        }
        //연차를 신청한기간에 이미 연차등록이있으면 예외처리
        List<AnnualLeaveRegister> annualLeaveRegisters = employee.getAnnualLeaveRegisters();
        annualLeaveRegisters.stream().forEach(it -> {
            LocalDate empStartDate = it.getStartDate();
            LocalDate empEndDate = it.getEndDate();
            if (!(endDate.isBefore(empStartDate) || startDate.isAfter(empEndDate))) {
                throw new IllegalArgumentException("신청한 휴가 기간이 이미 신청된 휴가 기간과 겹칩니다.");
            }
        });
        //연차 등록정보를 등록
        annualLeaveRepository.save(new AnnualLeaveRegister(employee, startDate, endDate));
        //연차를사용한 직원의 연차를 차감.
        int annualLeaveDays = (int) ChronoUnit.DAYS.between(startDate, endDate) + 1;
        //연차 신청기간이 직원의 잔여 연차보다 크면 예외처리
        if (annualLeaveDays > employee.getAnnualLeave()) {
            throw new IllegalArgumentException(
                    String.format("휴가 신청이 불가능합니다. 남은 연차 (%d) 신청한연차기간 (%d)", employee.getAnnualLeave(),
                            annualLeaveDays));
        }
        employee.useAL(annualLeaveDays);
    }

    @Transactional
    public void setAnnualLeave() {
        int newEmployee = getAnnualLeavePolicy("newEmployee");
        int experienced = getAnnualLeavePolicy("experienced");
        memberRepository.findAll().stream().forEach(employee -> {
            //올해
            int thisYear = LocalDate.now().getYear();
            //직원의 입사연도정보
            int employeeStartYear = employee.getStartDate().getYear();
            if (employeeStartYear == thisYear) {
                employee.setAnnualLeave(newEmployee);
            } else {
                employee.setAnnualLeave(experienced);
            }
        });
    }

    private int getAnnualLeavePolicy(String employeeStatus) {
        String policyGubn = "annualLeave";
        return Integer.parseInt(policyRepository.findByPolicyGubnAndPolicyName(policyGubn, employeeStatus)
                .orElseThrow(() -> new IllegalArgumentException("없는 정책입니다."))
                .getPolicyContent());
    }
}

 

코드가 길어져서 분리할까 했는데 우선은 포스팅용으로 한번에 볼수있게 두었다.

정리하면 이렇게 될것같다.

@Transactional
public void registerAl(ALRequest request) {
    LocalDate startDate = request.getStartDate();
    LocalDate endDate = request.getEndDate();
    Employee employee = memberRepository.findById(request.getEmployeeId())
            .orElseThrow(() -> new IllegalArgumentException("직원 정보가없습니다. "));
    //팀 휴가 사전보고일을 조회해서 사전보고일보다 늦게 보고시 에러처리
    validateExceedAdvancedReportDate(employee, startDate);
    //연차를 신청한기간에 이미 연차등록이있으면 예외처리
    validateALRedundant(employee, endDate, startDate);
    //연차 등록정보를 등록
    annualLeaveRepository.save(new AnnualLeaveRegister(employee, startDate, endDate));
    int annualLeaveDays = (int) ChronoUnit.DAYS.between(startDate, endDate) + 1;
    //연차 신청기간이 직원의 잔여 연차보다 크면 예외처리
    validateEmployeeRemainAL(annualLeaveDays, employee);
    //연차를사용한 직원의 연차를 차감.
    employee.useAL(annualLeaveDays);
}

 

저런 검증들도 객체를 분리하고 최대한 파라미터를 통일해서 하나의 메서드에서 처리할수있도록 나중에는 수정해야지..

 

이렇게 해주면 예외로두었던 케이스들을 커버할수있다.

코딩 전 미리 작성한 readme

예외 발생시 리턴해주는 객체도 처리해주었던데로 잘 리턴된다 

스프링부트 예외처리

 

스프링부트 예외처리 - @RestControllerAdvice

@RestControllerAdvice 스프링부트에서 @RestController를 이용해서 api를 개발하는경우에 예외처리시 이용한다. 예외를 처리하고 응답객체를 리턴해줄수있다. api응답으로 에러객체를만들어서 보내줄수있

colazoa.tistory.com

 

연차조회

연차를 조회하는 기능은 간단하게 구현했다. 테이블을 수정하면서 직원객체가 휴가의 일수를 가지고있기때문에

직원을 조회해서 가지고있는 휴가를 리턴해주면된다.

 

//Controller
@GetMapping
public ResponseAnnualLeave getAnnualLeave(Integer employeeId) {
    return service.getAnnualLeave(employeeId);
}
//DTO
@Data
@AllArgsConstructor
public class ResponseAnnualLeave {
    private Integer annualLeave;
}

//Service
 @Transactional(readOnly = true)
 public ResponseAnnualLeave getAnnualLeave(Integer employeeId) {
     Employee employee = memberRepository.findById(Long.valueOf(employeeId))
             .orElseThrow(() -> new IllegalArgumentException("직원 정보가없습니다. "));
     return new ResponseAnnualLeave(employee.getAnnualLeave());
 }

응답을 그냥 Integer로 받아도 괜찮지만 나중에 요구사항이 추가되어서 추가로 필드가 더 필요해질수도있고 

응답이나 요청을 스네이크 케이스로 변경해달라고하거나 할때

@JsonNaming어노테이션으로 유연하게 대처할수있을것같아서 응답객체를 만들어서 사용해주었다.

 

연차를 신청하고 조회할수있는 기능을 api에 추가해주었다. 

연차정보가 생기면서 다른 기능의 요구사항에 변경이 생겼는데 이 기능은 다음 포스팅에서 정리해보겠다.

 

 

반응형
반응형

@RestControllerAdvice 

스프링부트에서 @RestController를 이용해서 api를 개발하는경우에 예외처리시 이용한다.

예외를 처리하고 응답객체를 리턴해줄수있다. api응답으로 에러객체를만들어서 보내줄수있다.

@Transactional(readOnly = true)
public CommuteTotalResponse checkTime(CommuteGetDto request) {
    Employee employee = memberRepository.findById(request.getEmployeeId())
            .orElseThrow(IllegalArgumentException::new);
    CommuteTimeResponseList commuteTimeResponseList = new CommuteTimeResponseList(request, employee);

    return new CommuteTotalResponse(commuteTimeResponseList.getCommuteTimeResponseList());
}

 

기존에는 이렇게 서비스계층에서 예외를 처리해서 에러를 내게되면 500에러가 나면서 

이런 모양의 에러객체를 리턴해주게된다.

{
    "timestamp": "2024-03-15T09:31:41.095+00:00",
    "status": 500,
    "error": "Internal Server Error",
    "path": "/commute/worktime"
}

 

이 에러는 지금 내가요청하는 직원아이디를 없는아이디로 넣어서 나오는 에러인데

만약 이 API를 지금처럼 내가 직접만들고 해결한다면 백엔드코드를 보고 수정할수있을 것이다.

그러나 협업을 하게되고 클라이언트에서 요청했는데 이런 에러가나온다면

클라이언트측에서는 어떤 문제로 이런에러가 발생했는지 알기어려울 것이다.

이걸 해결하기 위해서 @RestControllerAdvice를 사용할수있다.

 

CommuteServiceExceptionHandler(에러 처리 객체)

@RestControllerAdvice
public class CommuteServiceExceptionHandler {
    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<String> illegalArgumentExceptionHandle(IllegalArgumentException e) {
        return ResponseEntity.badRequest().body("error : " + e.getMessage());
    }
}

 

이렇게 에러를 처리하는 객체를 만들고 @RestControllerAdvice를 달아주면

@RestController에서 에러가 발생하면 이 객체에서 전역적으로 처리해줄수있게된다. 

 

@ExceptionHandler(처리할예외.class)

여기에는 처리해줄 예외객체를 명시해주면 해당예외가발생시 해당하는 메서드를 실행시켜준다.

 

위의 코드는 IllegalArgumentException을 처리하는 메서드이다. 

service에서 아이디로검색한 직원이 없는경우에 IllegalArgumentException을 발생시키고

해당 예외가 이곳으로 와서 처리된다.

응답코드를 세팅하기위해서 리턴타입을 ResponseEntity로 해주었다.

ResponseEntity를 리턴해야 응답코드를 변경해서 보내줄수있으며

그냥 객체를 내보내게되면 해당 객체를 Json으로 보내주지만 응답객체는 200으로 나오게된다.

 

String으로 에러메시지를 리턴하는경우


 

ResponseEntity로 응답코드를 세팅해서 리턴하는경우

 

 

ResponseEntity<>로 특정객체를 감싸서 리턴해줄수도있다.

이번에는 api의 응답객체로 사용할 wrapper객체를 만들고 ResponseEntity로 이 객체를 감싸서 응답을 보내보자.

 

Api

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class)
public class Api<T> {

    private String resultCode;
    private String resultMessage;
    @Valid
    private T data;
    private Error error;

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    @Builder
    @JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class)
    public static class Error{
        private List<String> errorMessage;
    }
}

 

이 객체는 응답 결과데이터와 ,응답코드, 응답메시지,에러정보를 가지고있게된다 

만약 정상적인 요청을 보낸다면 

{
    "result_code": "200",
    "result_message": "OK",
    "data": {
        "name": "홍길동",
        "nick_name": "닉",
        "password": "1234",
        "email": "kim@gmail.com",
        "phone_number": "010-5491-2645",
        "register_at": "2024-03-16T09:58:14",
        "age": 26,
        "name_check": true
    },
    "error": null
}
 
이렇게 데이터에 응답객체를 가지고있고 응답코드,응답메시지도 표시해줄수있다. 
이 경우에 에러가 발생한다면.
 
 
{
    "result_code": "400",
    "result_message": "Bad Request",
    "data": null,
    "error": {
        "error_message": [
            "error :직원 정보가없습니다."
        ]
    }
}
 
이렇게 데이터는 빈값으로 주고 에러객체에 에러메시지값을 세팅해서 내보내줄수도있다. 
 

에러핸들러

@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<Api<? extends Objects>> illegalArgumentExceptionHandle(IllegalArgumentException e) {
    String message = "error :" + e.getMessage();
    Error error = Error.builder()
            .errorMessage(Collections.singletonList(message))
            .build();

    Api errorResponse = Api.builder()
            .resultCode(String.valueOf(HttpStatus.BAD_REQUEST.value()))
            .resultMessage(HttpStatus.BAD_REQUEST.getReasonPhrase())
            .error(error)
            .build();

    return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
}

 

이 경우에 Error객체를 만들어주고 Api객체에 세팅해주어야해서 코드는 약간 길어지지만

정리해서 따로 메서드로 분리한다면 더 깔끔하게 정리할수있을것같다. 

 

일반 사용자의 입장에서는 에러가 기분나쁘고 고장난것처럼 느껴지지만

개발하는 입장에서는 에러가 명확하고 어떤부분에서 에러가 발생하는지 확실하게 알려주는편이 더 좋다고 생각한다.

그래야 클라이언트측에서 실수를 한건지 백엔드에서 예외처리를 못해준건지 명확해지고

요청정보가 잘못되었다면 빠르게 수정할수있다. 

반응형
반응형

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

    @PostMapping("/worktime")
    public CommuteTotalResponse checkTime(@RequestBody CommuteGetDto request) {
        return service.checkTime(request);
    }
}

CommuteGetDto ( requestDTO)

@Getter
public class CommuteGetDto {
    private Long employeeId;
    @DateTimeFormat(pattern = "yyyy-MM")
    private YearMonth searchDate;
}

 

@DateTimeFormat은 json에서 문자열로오는 데이터를 YearMonth타입으로 매칭시켜주기위해서 사용했다.

 

Service

@Service
@RequiredArgsConstructor
@Slf4j
public class CommuteService {

    private final CommuteHistoryRepository repository;
    private final MemberRepository memberRepository;

    @Transactional
    public void checkWork(Long employeeId) {
        //해당 아이디의 직원을 조회
        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();
        }
    }
    
    @Transactional(readOnly = true)
    public CommuteTotalResponse checkTime(CommuteGetDto request) {
        Employee employee = memberRepository.findById(request.getEmployeeId())
                .orElseThrow(IllegalArgumentException::new);
        CommuteTimeResponseList commuteTimeResponseList = new CommuteTimeResponseList(request, employee);

        return new CommuteTotalResponse(commuteTimeResponseList.getCommuteTimeResponseList());
    }

}

 

@Transactional(readOnly = true) 는 데이터를 조회하기만 할때 사용한다.

이렇게사용하면 조회용 트랜잭션이 생성되서 성능적인 측면에서 좋다고 한다.

CommuteTimeResponse(DTO)

@Getter
@Setter
@NoArgsConstructor
public class CommuteTimeResponse {
    private String date;
    private Integer workingMinutes;
    private boolean usingDayOff;
}

CommuteTimeResponseList(일급컬렉션)

@Getter
public class CommuteTimeResponseList {
    List<CommuteTimeResponse> commuteTimeResponseList;

    public CommuteTimeResponseList(CommuteGetDto request, Employee employee) {
        List<CommuteTimeResponse> collect = employee.getCommuteHistories().stream()
                //연월 필터링
                .filter(result ->
                        result.isSameYearMonth(request.getSearchDate())
                        && !result.getIsWorking()
                        )
                //그루핑
                .collect(Collectors.groupingBy(
                        result -> dateTimeToFormatString(result.getStartTime(), "yyyy-MM-dd"),
                        Collectors.summingInt(result -> diffMinute(result.getStartTime(), result.getEndTime()))
                ))
                .entrySet().stream()
                .map(result -> {
                    CommuteTimeResponse response = new CommuteTimeResponse();
                    response.setDate(result.getKey());
                    response.setWorkingMinutes(result.getValue());
                    return response;
                }).collect(Collectors.toList());
                
                //정렬
        	List<CommuteTimeResponse> list = collect.stream()
                	.sorted(Comparator.comparing(response -> LocalDate.parse(response.getDate())))
                	.toList();

        this.commuteTimeResponseList = list;
    }

    private String dateTimeToFormatString(LocalDateTime time, String format) {
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern(format);
        return time.format(formatter);
    }

    private Integer diffMinute(LocalDateTime start, LocalDateTime end) {
        Duration duration = Duration.between(start, end);
        return (int) duration.toMinutes();
    }
}

 

아직 리팩토링이 더필요한 부분이긴한데.. 서비스에서 출퇴근리스트를 필터링하고 그룹화해줘서 만들었던 메서드를 일급컬렉션으로 옮기고 응답을 해주는 객체인 CommuteTimeResponse로 매핑해주는 메서드 들이다. 이런 부분들을 jpql로 애초에 응답객체의 형식에맞춰서 데이터를 조회해올수도있지만 직원객체에서 출퇴근데이터를 이미 가지고있어서 나는 그냥 있는데이터를 처리해서 응답객체를 만들어주는 방법을 사용했다. 성능적으로 뭐가 더 좋은지는 잘 모르겠다. 

 

CommuteTotalResponse(DTO)

@Getter
@Setter
public class CommuteTotalResponse {
    private List<CommuteTimeResponse> detail = new ArrayList<>();
    private Integer sum;

    public CommuteTotalResponse(List<CommuteTimeResponse> detail) {
        this.detail = detail;
        this.sum = detail.stream().mapToInt(CommuteTimeResponse::getWorkingMinutes).sum();
    }
}

조회해서 매핑한데이터를 이클래스로 감싼다음 리턴해주어야 위에서봤던 Json의 응답형식에 맞출수있다.

{
    "detail": [
        {
            "date": "2024-03-01",
            "workingMinutes": 0,
            "usingDayOff": true
        },
        {
            "date": "2024-03-02",
            "workingMinutes": 0,
            "usingDayOff": true
        },
        {
            "date": "2024-03-03",
            "workingMinutes": 180,
            "usingDayOff": false
        },
        {
            "date": "2024-03-04",
            "workingMinutes": 120,
            "usingDayOff": false
        },
        {
            "date": "2024-03-05",
            "workingMinutes": 118,
            "usingDayOff": false
        },
        {
            "date": "2024-03-06",
            "workingMinutes": 0,
            "usingDayOff": true
        },
        {
            "date": "2024-03-07",
            "workingMinutes": 0,
            "usingDayOff": true
        },
        {
            "date": "2024-03-11",
            "workingMinutes": 0,
            "usingDayOff": false
        }
    ],
    "sum": 418
 

 

이렇게 직원정보와 검색날짜에 맞춰서 데이터가 잘 조회되는것을 볼수있다 usingDayOff부분은

이후에 진행하면서 추가된 기능의 부분이니 지금은 무시하자.


추가수정

CommuteController

package Company.demo.commute.controller;

import Company.demo.commute.dto.request.CommuteGetDto;
import Company.demo.commute.dto.response.CommuteTotalResponse;
import Company.demo.commute.service.CommuteService;
import java.util.Map;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

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

	//PostMapping 이던 부분을 Get으로 변경 (조회하는 메서드에 알맞게 수정)
    @GetMapping("/worktime")
    public CommuteTotalResponse checkTime(CommuteGetDto request) {
        System.out.println("request.getSearchDate() = " + request.getSearchDate());
        System.out.println("request.getEmployeeId() = " + request.getEmployeeId());
        return service.checkTime(request);
    }
}

 

CommuteTimeResponseList

@Getter
public class CommuteTimeResponseList {
    List<CommuteTimeResponse> commuteTimeResponseList;

    public CommuteTimeResponseList(CommuteGetDto request, Employee employee) {
        List<CommuteTimeResponse> collect = filterAndGroupCommuteHistries(request, employee);
        //연차 정보를 더해준다.
        addAnnualLeaveToCommuteTimeResponse(request, employee, collect);
        //정렬
        this.commuteTimeResponseList = sortCommuteTimeResponseListbyDate(collect);
    }

    private static List<CommuteTimeResponse> sortCommuteTimeResponseListbyDate(List<CommuteTimeResponse> collect) {
        List<CommuteTimeResponse> list = collect.stream()
                .sorted(Comparator.comparing(response -> LocalDate.parse(response.getDate())))
                .toList();
        return list;
    }

    private static void addAnnualLeaveToCommuteTimeResponse(CommuteGetDto request, Employee employee, List<CommuteTimeResponse> collect) {
        employee.getAnnualLeaveRegisters().stream()
                .filter(leave -> {
                    YearMonth startYearMonth = YearMonth.from(leave.getStartDate());
                    YearMonth endYearMonth = YearMonth.from(leave.getEndDate());
                    return !startYearMonth.isAfter(request.getSearchDate()) && !endYearMonth.isBefore(request.getSearchDate());
                })
                .forEach(leave -> {
                    LocalDate startDate = leave.getStartDate();
                    LocalDate endDate = leave.getEndDate();

                    // 검색 시작일과 휴가 시작일 중 늦은 날짜를 선택
                    LocalDate searchStart = request.getSearchDate().atDay(1);
                    LocalDate effectiveStartDate = startDate.isBefore(searchStart) ? searchStart : startDate;

                    // 검색 종료일과 휴가 종료일 중 이른 날짜를 선택
                    LocalDate searchEnd = request.getSearchDate().atEndOfMonth();
                    LocalDate effectiveEndDate = endDate.isAfter(searchEnd) ? searchEnd : endDate;

                    long daysBetween = ChronoUnit.DAYS.between(effectiveStartDate, effectiveEndDate);
                    for (long i = 0; i <= daysBetween; i++) {
                        LocalDate currentDate = leave.getStartDate().plusDays(i);
                        collect.add(new CommuteTimeResponse(currentDate));
                    }
                });
    }

    private List<CommuteTimeResponse> filterAndGroupCommuteHistries(CommuteGetDto request, Employee employee) {
        List<CommuteTimeResponse> collect = employee.getCommuteHistories().stream()
                //연월 필터링
                .filter(result ->
                        result.isSameYearMonth(request.getSearchDate())
                                && !result.getIsWorking()
                )
                //그루핑
                .collect(Collectors.groupingBy(
                        result -> dateTimeToFormatString(result.getStartTime(), "yyyy-MM-dd"),
                        Collectors.summingInt(result -> diffMinute(result.getStartTime(), result.getEndTime()))
                ))
                .entrySet().stream()
                .map(result -> {
                    CommuteTimeResponse response = new CommuteTimeResponse();
                    response.setDate(result.getKey());
                    response.setWorkingMinutes(result.getValue());
                    return response;
                }).collect(Collectors.toList());
        return collect;
    }

    private String dateTimeToFormatString(LocalDateTime time, String format) {
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern(format);
        return time.format(formatter);
    }

    private Integer diffMinute(LocalDateTime start, LocalDateTime end) {
        Duration duration = Duration.between(start, end);
        return (int) duration.toMinutes();
    }
}

 

 

객체를 매핑해주는 코드를 수정해줬다

필터링하고 그룹을 지어주는 메서드

연차정보를 매핑해서 추가해주는 메서드

리스트를 날짜정보에의해 정렬해주는 메서드 , 이 세가지로 분리해서 리팩토링해주고 

 

연차정보를 추가하는 메서드는 필터조건과 추가하는 방법이 수정되었다.

이전의 방법은 시작일자의 연월데이터와 검색일자의 연월데이터를 비교해서 같은것들을 필터링해주었는데 

이런 필터링방법을 쓰니 내 데이터의 특성상 시작일자는 3월 이더라도 끝나는일자가 4월인 데이터들이 같이 추가되었다.

.filter(leave -> {
    YearMonth startYearMonth = YearMonth.from(leave.getStartDate());
    YearMonth endYearMonth = YearMonth.from(leave.getEndDate());
    return 
    //연차시작일자가 검색일자와 같거나 이전인 데이터 //검색일자보다 이후이면 false
    !startYearMonth.isAfter(request.getSearchDate()) 
    && 
    //연차 끝나는일자가 검색일자보다 같거나 이후인 데이터 //검색일자보다 이전이면 false
    !endYearMonth.isBefore(request.getSearchDate());
    //두조건이 and조건이므로 검색일자를 포함하는 데이터만남는다.
})

 

ex )

검색일자는 2024-03 

휴가시작일자는 2024-02

휴가끝나는일정이 2024-04 

 

이런경우에는 검색일자가 휴가일정에 포함되어있으므로 true가 나와야한다. 

그래서 검색일자와 같거나 이전인데이터(true) && 검색일자와 같거나 이후인데이터(true) 

이렇게 조건을 잡도록 수정해주었다. 이렇게 필터링을하니 2월 3월 4월 같은 검색날짜 이전과 이후의 데이터도 필터를 통과하는데.

.forEach(leave -> {
    LocalDate startDate = leave.getStartDate();
    LocalDate endDate = leave.getEndDate();

    // 검색 시작일과 휴가 시작일 중 늦은 날짜를 선택
    //검색월의 첫째날 if 2024-03이면 3월1일
    LocalDate searchStart = request.getSearchDate().atDay(1);
    //만약 휴가시작일이 2월10일 이라면 2월10일은 검색일인 3월보다이전이므로 true -searchStart를 리턴
    LocalDate effectiveStartDate = startDate.isBefore(searchStart) ? searchStart : startDate;

    // 검색 종료일과 휴가 종료일 중 이른 날짜를 선택
    // 검색월의 마지막날 ex) 3월31일
    LocalDate searchEnd = request.getSearchDate().atEndOfMonth();
    // 만약 휴가의 마지막날이 검색의 마지막날보다 이후라면 searchEnd 리턴
    LocalDate effectiveEndDate = endDate.isAfter(searchEnd) ? searchEnd : endDate;
    
    //2월부터 4월까지가 휴가라면 각 리턴된결과에따라 3월1일부터 3월 31일까지가 startDate,endDate가 된다.
    long daysBetween = ChronoUnit.DAYS.between(effectiveStartDate, effectiveEndDate);
    for (long i = 0; i <= daysBetween; i++) {
        LocalDate currentDate = leave.getStartDate().plusDays(i);
        collect.add(new CommuteTimeResponse(currentDate));
    }
});

 

 

이렇게 몇번 실행을하고 리팩토링을 하다보니 처음에는 생각하지못했던 문제들도 마주하게되었다. 

이런 케이스들을 여러번 경험하다보면 경험적으로 미리 이런부분들을 방지할수도 있게되겠지.

반응형
반응형

 

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

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

 

출퇴근 기능

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 , 일급컬렉션

반응형

+ Recent posts