Skip to content

strangehoon/Kinder-grew

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🖐킨더그루

M1

📝 개요


🕸 아키텍쳐



⚒ 기술스택


💡 주요 기능

접기/펼치기
  • 1. 간편한 소셜 로그인



  • 2. 등하원 관리 서비스

    • 실시간 등 하원 처리


    • 실시간 알림 기능(학부모 등,하원 카톡 전송)


  • 3. 출결 관리 서비스

    • 월별 출석부(+ 엑셀 내보내기 기능)


    • 일별 출석부(+ 엑셀 내보내기 기능)


    • 결석 신청 및 취소


  • 4. 유치원 운영관리 서비스
    • 반 별 갤러리



    • 아이 정보 열람


📝 API 명세

킨더그루 팀 노션 바로가기


📊 ERD

🤝 협업방식

TEAM RULE

  • 전체회의시간: 저녁 8시
  • BE 회의시간: 저녁 5시 30분
    • 코드리뷰
    • 이슈 공유하기
    • 진행 상황 공유하기
  • 적어도 회의 시간에는 비디오와 음성 키기
  • 기능 단위로 커밋하기
  • pr 템플릿 양식 준수하기
  • Issue 템플릿 양식 준수하기
  • 머지하고 나서 브랜치 꼭 지우기
  • 풀 당기기 전에 패치하기
  • 프로그래밍 컨벤션 준수하기

⚖️ 기술적 의사결정


기술 선택지 이유
Redis 1. DB 저장
2. Redis
유치원이라는 특성상, 선생님이 접속을 오래 하고 있기 때문에 Refresh Token이 필요하다고 생각이 들었다.
Refresh Token을 DB에 저장 해서 사용을 해도 되지만, 그렇게 되면 스케줄러를 사용해서 직접 만료 된 Refresh Token을 삭제 해줘야 하기도 하고 , 캐시인 Redis가 더 가볍고 속도도 빠르고 TTL을 통해서 자동으로 삭제도 가능하기 때문에 Redis를 적용 해보기로 했다.
ImageIO 1. Marvin open source Library 2. Graphics2D 3. ImageIO 처음에는 이미지를 S3에 업로드만 했기 때문에 별도의 이미지에 대한 처리를 하지 않았지만, 이미지를 리사이징해야 할 필요성이 생기면서 이미지 리사이징 방법을 고민해야 했다. 가장 먼저 Marvin open source Library를 사용해 리사이징했지만 이미지가 심해게 도트화되는 문제와 프로젝트 전체 용량보다 marvin 라이브러리의 용량이 약 3배 더 컸으며, 처리 성능 저하 문제도 있었다. 따라서 java.awt 패키지의 Graphics2D 클래스를 이용해 별도의 라이브러리 추가 사용 없이 구현을 해 보았지만 성능은 개선되었지만 도트화가 더욱 심각해지는 문제가 있었다. 따라서 Graphics2D 대신 ImageIO클래스를 이용해 리사이징하는 방법을 채택했다. 도트화가 아예 없는 것은 아니었지만 다른 2개의 방법에 비해 정도가 낮았으며 별도의 라이브러리 설치도 필요없었고, 성능도 Graphics2D와 큰 차이를 보이지 않았기 때문이다.
카카오 알림 기능 1. 프론트엔드에서 알림 구현
2. 백엔드에서 알림 구현
프론트엔드, 백엔드 모두 카카오 알림 기능을 구현할 수 있지만 다음과 같은 이유로 백엔드에서 처리하기로 했다.
1. 트랜잭션
단순 카카오 알림 기능 뿐만 아니라 아이의 등하원 상태도 바뀌어야 하므로 프론트엔드에서 처리 시 카카오 알림 메시지 API 뿐만 아니라 등하원 상태 변경 API도 필요했다. 하지만 서버에서는 API 하나로 같은 트랜잭션에서 처리할 수 있다. 이로인해 카카오 알림 기능과 등하원 상태 변경을 묶어서 일관성을 보장할 수 있었다.
2. 보안
카카오 메시지 API를 사용하는 경우, 보안상 중요한 kakaoId와 AccessToken이 필요하다. 이를 프론트엔드에서 처리하면, 이 정보가 브라우저에서 노출되거나 탈취될 수 있다. 따라서 백엔드에서 처리하면, 안전한 환경에서 이 정보들을 처리할 수 있다.
복잡한 동적 쿼리 작성 1. JPA 쿼리 메서드
2. @Query
3. QueryDSL
기존의 JPA 쿼리 메서드는 동적 쿼리를 작성하는데 한계가 있었다. 그래서 스프링 데이터 JPA의 @Query를 사용하려 했다. @Query도 동적 쿼리 작성이 가능하므로 좋은 대안이라고 생각했으나 그래도 주어진 문제에 적용하기에는 고려해야 할 조건이 너무 많다고 생각했다. 무엇보다도 가독성이 너무 떨어져 유지보수하기 어렵다고 생각했다. 반면 QueryDSL의 where 다중 파라미터 방식은 주어진 문제의 조건들을 동적으로 커스튬할 수 있을 거라 생각했다. 이 외에도 컴파일 에러를 잡을 수 있을 뿐만 아니라 @Query보다 쿼리 자체의 가독성이 훨씬 좋다는 점도 QueryDSL을 도입한 이유였다.

🔨 트러블슈팅


🛠 프로젝트 후 혼자서 진행한 리팩토링

다음은 프로젝트가 끝나고 이상훈(strangehoon)가 혼자서 진행한 내용들이다.

1. QueryDSL 성능 개선

💡 자세한 내용은 QueryDSL을 이용한 동적 쿼리 생성 및 성능 개선 참고

이전에 작성한 코드에서 다음과 같은 사항들을 고려하여 리팩토링했다.

First : 불필요한 cross join과 distinct 메서드 제거
Second : where절 중복 조건 제거
Third : attendance 테이블의 date 컬럼 인덱스 적용
Fourth : PageableExecutionUtils를 통한 count 쿼리 최적화

그 결과 쿼리 실행시간 기준으로 34.4 + α 배 성능 향상이 있었다.
(attendance : 22000 rows, child : 60 rows, classroom : 3 rows, kindergarten 1 rows)

수정한 QueryDSL 코드

@RequiredArgsConstructor
@Repository
public class ChildRepositoryImpl implements ChildRepositoryCustom{

    private final JPAQueryFactory queryFactory;

    @Override
    public Page<ChildScheduleResponseDto> findChildSchedule(Long classroomId, Long kindergartenId, CommuteStatus commuteStatus, String time, Pageable pageable,
                                                                              InfoDto info, List<ClassroomInfoDto> everyClass){
        List<ChildScheduleResponseDto> result = queryFactory
                .select(new QChildScheduleResponseDto(
                        child.id,
                        child.name,
                        child.profileImageUrl,
                        attendance.enterTime,
                        attendance.exitTime,
                        attendance.status
                ))
                .from(attendance)
                .join(attendance.child, child)
                .join(child.classroom, classroom)
                .join(classroom.kindergarten, kindergarten)
                .where(classroomIdAndKindergartenIdIs(classroomId, kindergartenId), stateIs(commuteStatus),
                        timeIs(commuteStatus, time))
                .orderBy(child.name.asc())
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetch();

        JPAQuery<Attendance> total = queryFactory
                .select(attendance)
                .from(attendance)
                .join(attendance.child, child)
                .join(child.classroom, classroom)
                .join(classroom.kindergarten, kindergarten)
                .where(classroomIdAndKindergartenIdIs(classroomId, kindergartenId), stateIs(commuteStatus),
                        timeIs(commuteStatus, time));

        return PageableExecutionUtils.getPage(result, pageable, total::fetchCount);
    }

    private BooleanExpression classroomIdAndKindergartenIdIs(Long classroomId, Long kindergartenId) {
        return classroomId != null ? child.classroom.id.eq(classroomId).and(classroom.kindergarten.id.eq(kindergartenId)) : classroom.kindergarten.id.eq(kindergartenId);
    }

    private BooleanExpression stateIs(CommuteStatus commuteStatus){
        if(commuteStatus.equals(ENTER)){
            return attendance.exitTime.isNull().and(attendance.date.eq(LocalDate.now())).and(attendance.status.ne(결석));
        }
        else if(commuteStatus.equals(EXIT)) {
            return attendance.enterTime.isNotNull().and(attendance.date.eq(LocalDate.now())).and(attendance.status.ne(결석));
        }
        else
            return null;
    }

    private BooleanExpression timeIs(CommuteStatus commuteStatus, String time) {
        if(commuteStatus.equals(ENTER)){
            return time != null ? child.dailyEnterTime.eq(time) : null;
        }
        else
            return time != null ? child.dailyExitTime.eq(time) : null;
    }
}

2. N+1 문제 해결

💡 자세한 내용은 N+1 문제 해결 참고

반별 해당 날짜의 출결 내역을 조회하는 findAttendanceDate 메서드에서 N+1 문제가 발생했다. 컬렉션인 child의 List를 조회했는데 child와 연관된 attendance 조회 쿼리 1개가 추가로 db에 나갔다. 만약 child가 수십, 수백 명이면 그에 따라 attendance 조회 쿼리가 수십, 수백개가 나가므로 서비스에 심각한 장애가 일어날 수 있다고 판단했고 다음과 같은 해결책들을 구상했다.

First : OneToMany, 페치조인
Second : OneToMany, Dto 조회
Third : OneToMany, Dto 조회, ,Where절 in
Fourth : ManyToOne, 페치조인
Fifth : ManyToOne, Dto 조회

필요한 데이터가 child와 attendance 테이블에 반반으로 섞여 있어서 OneToMany로 child를 기준으로 삼든 ManyToOne으로 attendance를 기준으로 삼든 상관없다고 판단했다. 하지만 child : attendance가 대략 1 : 369로 child 쪽을 기준으로 삼기에는 부담이 커보였다. 따라서 ManyToOne으로 attendance를 기준으로 데이터를 조회해오면 성능상 이점이 있을 거라 생각했다. 또한 Dto로 직접 조회하는 방식보다는 엔티티 조회를 통한 페치조인 방식이 코드를 거의 수정하지 않고, 옵션만 약간 변경해서, 다양한 성능 최적화를 시도할 수 있어서 결국 4번째 방식을 선택했다.

그 결과는 다음과 같다.

기존

  • n+1 문제 : 발생 O
  • 쿼리 수 : 1+20
  • 실행 시간 : 238ms

리팩토링 후

  • n+1 문제 : 발생 X
  • 쿼리 수 : 1
  • 실행 시간 : 18ms

(attendance : 22000 rows, child : 60 rows, classroom : 3 rows, kindergarten 1 rows)

@Transactional(readOnly = true)
public GlobalResponseDto findAttendanceDate(Long classroomId, Long kindergartenId, String date){
		
    ...

    List<DateAttendanceResponseDto> attendanceResponseDtoList = new ArrayList<>();
    List<Attendance> attendanceList = attendanceRepository.findAttendanceListByDate(LocalDate.parse(date), classroomId);
    for(Attendance attendance : attendanceList){
        attendanceResponseDtoList.add(new DateAttendanceResponseDto(attendance));
    }

    ...
}

@Query("select a from Attendance a join fetch a.child c where a.date = :date and c.classroom.id =:classroomId")
List<Attendance> findAttendanceListByDate(LocalDate date, Long classroomId);

@Data
public class DateAttendanceResponseDto{
    private Long id;

    private String name;

    private AttendanceStatus status;

    @JsonFormat(pattern = "HH:mm")
    private LocalTime enterTime;

    @JsonFormat(pattern = "HH:mm")
    private LocalTime exitTime;

    private String absentReason;

    public DateAttendanceResponseDto(Attendance attendance){
        id = attendance.getChild().getId();
        name = attendance.getChild().getName();
        status = attendance.getStatus();
        enterTime = attendance.getEnterTime();
        exitTime = attendance.getExitTime();
        absentReason = attendance.getAbsentReason();
    }
}

About

프로젝트 : 킨더그루

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Java 100.0%