spring_2기[본캠프]/과제

[과제] Spring e-Commerce back office Task Day 2

minwoo95 2026. 1. 15. 23:04

https://github.com/Hyeonseok-Homies/Spring_e-commerce-back-office

 

GitHub - Hyeonseok-Homies/Spring_e-commerce-back-office

Contribute to Hyeonseok-Homies/Spring_e-commerce-back-office development by creating an account on GitHub.

github.com

 

첫 개발 하는 팀 프로젝트라 이미지 자료를 미리 저장하지 못하고, 쫒기는 스케줄에 내용으로나마 정리를 해야겠다...

 

트러블슈팅

1. 개발간 페이징 부분을 자바 if for문으로 구현하려다가 너무 복잡하게 로직이 구현되어서 찾아서 JPQL 로 구현하였다.

@Query(
    "SELECT a FROM Admin a WHERE "
        + // Admin 객체(테이블) a를 모두 객체 a 전체 조회하겠다. 어떻게?(조건)
        "(:kw IS NULL OR a.name LIKE %:kw% OR a.email LIKE %:kw%) AND "
        +
        // 외부(:) 변수(kw)[입력 검색어]에 값이 비어있나? or a테이블에 name(DB에 저장된 이름)이 있으면 LIKE(~같은지 봐라) kw앞뒤로 어떤내용이
        // 와도 상관없고(%)
        // 또는(OR) a테이블에 email이 LIKE(~같은지) kw앞뒤로 어떤내용이 와도 상관없다(%)
        // 즉, 입력한 키워드가 테이블에 있는 이름과 이메일이중에서 유사값이 있는지 찾아라
        "(:role IS NULL OR a.role = :role) AND "
        +
        // (:)외부에서 온 role이 널인지 또는 a테이블에 role이 입력 role과 일치하는지 그리고
        "(:status IS NULL OR a.status = :status)")
// (:)외부에서 온 status가 널인지 또는 a테이블에 status가 입력 status과 일치하는지
Page<Admin> searchAdmins(
    @Param("kw") String kw,
    // 한페이지 분량만 가져오는데 내용물은 <Admin>이다 + 메서드이름 +  @Param("kw"): 쿼리문 안에 적었던 :kw 자리와 이 변수 kw를 서로
    // 연결(바인딩)하라는 명령
    @Param("role") AdminRole role,
    @Param("status") AdminStatus status,
    Pageable pageable); 

->추가로 1차 캐싱에서 찾는건지 DB에 다녀온건지 궁금하였다.

->결론은 1차 캐싱에 없는 내용이면 DB에서 쿼리를 수행후 결과를 1차 캐쉬로 가져와 참조하게 된다

 

2.페이징 구현에 있어서 문법과 작동원리에 대해 이해가 안되어서 어려웠다 

->자료를 찾아서 구현을 완료하였다!

Pageable pageable =
    PageRequest.of( // 아래 내용들을 모두 챙겨 하나로 묶어 new Pageable 객체를 생성하는것????????
        requestDto.getPage() - 1, // 1페이라고 입력이 와도 0번째 인덱스부터 접근???????->-1이면 어떻게 하지?
        // 프론트와 1을 보낼지 0을 보낼지 명세서로 약속을 해야한다
        requestDto.getPageSize(),
        Sort.by(Sort.Direction.fromString(requestDto.getDirection()), requestDto.getSortBy())
        // 정렬방향 Sort.Direction.fromString(requestDto.getDirection())
        // requestDto.getSortBy() 어떤 필드를 기준으로 정렬할지
        );

 

3.깃 푸시간 관리자 회원가입과 로그인을 구현한 맴버와 라인이 겹쳐 오류가 대량 발생

->다행이 라인이 겹친 오류라 복붙으로 라인이 겹치지 않게 재배치 하여 오류를 해결하였다.

    다음부터는 주석으로 표시하여 해당 라인안에서만 각자 작업할수 있도록 해야겠다

 

4.내가 개발한 조회 기능들이 로그인을 해야 작동하는것으로 구현하였는데, 로그인 개발된 세션 메서드클래스와 변수명 차이로 오류가 발생

 

5.로그인을 했는데 접속자가 적절한 권한을 가지고 있는지에 대한 검증은 없어서 수정하라는 피드백이 있었다.

 

 

피드백 반영 결과

 

6.엔티티 디테일 수정

@Column(length = 255) 와 같이 길이나 null 에 대한 옵션을 추가하여 DB 스키마와 JPA엔티티 결합성을 증가 및 데이터 검증 

 

 

7. @RequestMapping으로 클래스 상단에 있는 공통 URL을 선언하여 중복을 제거

 

8.자바의 문법이 아닌 JPQL 을 사용했는가에 대한 정확한 코어 개념 확립하기

1.DB가 잘하는것은 DB가 하도록->JPQL로 원하는 데이터만 가져오기

Stream if for 를 사용하면 DB데이터를 모두 전부다 가져와야 한다.(낭비)

2.복잡한 동적 쿼리를 가장 직관적으로 해결할수 있다(가독성이 좋아진다)

3. Pageable 사용시 효율성이 좋아진다.

->Pageable을 넘기면 Spring Data JPA가 자동으로 LIMIT, OFFSET 쿼리뿐만 아니라 전체 개수를 세는 COUNT 쿼리까지 만들어 주기때문에 자바로 구현하는것보다 훨씬 간단하게 작업이 가능하다

 

근데 만약 동적으로 쿼리를 만드는데, 조건이 많아 진다면? JPQL 사용이 적절한가?에 대한 생각을 해봐야 한다고 한다

-WHERE 절이 엄청 늘어난다 코드가 길어짐으로 오류가 날 확률이 높아진다

-JQPL은 문자열임으로 컴파일 에러 체크가 불가능 함으로 실행전까지는 오류 확인이 불가

-모든 조건을 검사하게 되어서 조건이 많아진다면, 성능이 저하된다

결론, 조건이 늘어난다면 오타에 취약하고 가독성이 떨어진다

 

해결 방법으로 QueryDSL 기술을 활용하자!

-자바코드로 작성되기 때문에 컴파일러가 오류 체크를 해준다

-if나 BooleanBuilder를 사용해 조건이 있을때만 쿼리 문장을 추가 할수 있다

-코드가 깔끔해 진다.

 

9.Pageable객체를 수동으로 만들지 않고 자동화 하기

// 1. 관리자 목록 조회
@GetMapping
public ResponseEntity<AdminListResponseDto> getAdminList(
    @Login SessionAdmin sessionAdmin,
    @RequestParam(required = false) String kw,
    @RequestParam(required = false) AdminRole role,
    @RequestParam(required = false) AdminStatus status,
    @PageableDefault(size = 10, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable) {
  return ResponseEntity.ok(adminService.getAdminList(kw, role, status, pageable));
}
// 1. [관리자 리스트 조회] 검색, 페이징, 역할/상태 필터 적용
public AdminListResponseDto getAdminList(String kw, AdminRole role, AdminStatus status, Pageable pageable) {
    /*
  // 검색 규칙 설정
  Pageable pageable =
      PageRequest.of( // 아래 내용들을 모두 챙겨 하나로 묶어 new Pageable 객체를 생성하는것????????
          requestDto.getPage() - 1, // 1페이라고 입력이 와도 0번째 인덱스부터 접근???????->-1이면 어떻게 하지?
          // 프론트와 1을 보낼지 0을 보낼지 명세서로 약속을 해야한다
          requestDto.getPageSize(),
          Sort.by(Sort.Direction.fromString(requestDto.getDirection()), requestDto.getSortBy())
          // 정렬방향 Sort.Direction.fromString(requestDto.getDirection())
          // requestDto.getSortBy() 어떤 필드를 기준으로 정렬할지
          );

     */
  /*
  // 엔티티->DTO 변환
  List<AdminResponseDto> list =
      adminPage.getContent().stream() // .getContent() Admin 객체를 꺼내서 .stream()하나씩
          .map(AdminResponseDto::new) // 매핑해라 새로운 AdminResponseDto객체를 만들어서 Admin를 넣어라
          .collect(Collectors.toList()); // 변환된 AdminResponseDto객체를 한바구니에 담아라 ->List



  return new AdminListResponseDto(
      list,
      adminPage.getNumber() + 1,
      adminPage.getTotalPages(),
      adminPage.getTotalElements(),
      adminPage.getSize(),
      adminPage.hasNext());*/

    // 1. Repository에서 Page 객체로 데이터를 가져옵니다.
    Page<Admin> adminPage = adminRepository.searchAdmins(kw, role, status, pageable);

    // 2. Page 객체 안의 내용을 DTO 리스트로 변환 (빨간불 해결을 위해 생성자 확인 필수!)
    List<AdminResponseDto> list = adminPage.getContent().stream()
            .map(AdminResponseDto::new)
            .collect(Collectors.toList());

    // 3. 기존에 사용하던 AdminListResponseDto에 Page 객체가 가진 정보를 넣어준다
    return new AdminListResponseDto(
            list,
            adminPage.getNumber() + 1,    // 현재 페이지 (0부터 시작하므로 +1)
            adminPage.getTotalPages(),     // 전체 페이지 수
            adminPage.getTotalElements(),  // 전체 데이터 갯수
            adminPage.getSize(),           // 한 페이지당 요청 갯수
            adminPage.hasNext()            // 다음 페이지 여부
    );
}

 

10.LoginArgumentResolver를 구현하여 코드 간결화와 null 처리와 타입의 안정화 코드 재사용성 증가가 되겠금 수정

package com.backoffice.admin.config;

import com.backoffice.admin.dto.SessionAdmin;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

public class LoginArgumentResolver implements HandlerMethodArgumentResolver {

  @Override
  public boolean supportsParameter(MethodParameter parameter) {
    // 세션타입 확인
    boolean hasLoginUserAnnotation = parameter.hasParameterAnnotation(Login.class);
    boolean isSessionDtoType = SessionAdmin.class.isAssignableFrom(parameter.getParameterType());

    return hasLoginUserAnnotation && isSessionDtoType;
  }

  @Override
  public Object resolveArgument(
      MethodParameter parameter,
      ModelAndViewContainer mavContainer,
      NativeWebRequest webRequest,
      WebDataBinderFactory binderFactory)
      throws Exception {
    HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
    HttpSession session = request.getSession(false);
    if (session == null) {
      throw new IllegalStateException("로그인이 필요합니다.");
    }
    Object attribute = session.getAttribute("loginAdmin");
    if (attribute == null) {
      throw new IllegalStateException("로그인이 필요합니다.");
    }
    if (!(attribute instanceof SessionAdmin)) {
      // 안맞는 데이터 제거
      session.invalidate();
      throw new IllegalStateException("데이터가 올바르지 않습니다.");
    }
    SessionAdmin sessionAdmin = (SessionAdmin) attribute;
    return sessionAdmin;
  }
}
@Transactional(readOnly = true)
public AdminLoginResponse login(@Valid AdminLoginRequest request) {
  Admin admin =
      adminRepository
          .findByEmail(request.getEmail())
          .orElseThrow(() -> new IllegalStateException("존재하지 않는 이메일입니다."));
  if (!passwordEncoder.matches(request.getPassword(), admin.getPassword())) {
    throw new IllegalStateException("비밀번호가 일치하지 않습니다.");
  }
  switch (admin.getStatus()) {
    case INACTIVE -> throw new IllegalStateException("계정이 비활성 상태입니다.");
    case SUSPENDED -> throw new IllegalStateException("계정이 정지 상태입니다.");
    case PENDING -> throw new IllegalStateException("계정이 승인대기 상태입니다.");
    case REJECTED -> throw new IllegalStateException("계정이 거부 상태입니다.");
  }

  return new AdminLoginResponse(
      admin.getId(), admin.getName(), admin.getStatus(), admin.getRole(), admin.getEmail());
}

 

11.포스트맨 테스트시, 슈퍼관리자 계정이 없어 진행이 불가하였다.

1.인설트 쿼리문으로 슈퍼관리자 계정을 넣었더니 비밀번호가 인코딩되어 들어가지 않아 로그인시 비밀번호 일치여부 비교할때 불일치가 발생하였다.

2.매번 테스트마다 계정을 생성해주는 번거러움이 발생

@Component
@RequiredArgsConstructor
public class AdminInitializer implements CommandLineRunner {

    private final AdminRepository adminRepository;
    private final PasswordEncoder passwordEncoder;

    @Override
    @Transactional
    public void run(String... args) {
        // 이미 계정이 있으면 더 안 만들게 방어 로직 (멱등성)
        if (!adminRepository.existsByEmail("admin@example.com")) {

            Admin admin = Admin.builder()
                    .name("Admin")
                    .email("admin@example.com")
                    .password(passwordEncoder.encode("12345678"))
                    .phoneNumber("010-1234-5678")
                    .role(AdminRole.SUPER)
                    .status(AdminStatus.ACTIVE)
                    .createdAt(LocalDateTime.now())
                    .build();

            adminRepository.save(admin);
            System.out.println("=== 슈퍼관리자 계정 생성 완료 (admin@example.com / 12345678) ===");
        }
    }
}

클래스를 생성하여 계정을 실행시마다 자동생성 될수 있게 조치

비밀번호를 인코딩하여 주입함으로, 포스트맨 로그인시 비밀번호 불일치 오류 해결