본문 바로가기

삽질

Pillin 프로젝트 백엔드 개발 리뷰

https://m.onestore.co.kr/mobilepoc/apps/appsDetail.omp?prodId=0000774295

 

필린 - 원스토어

여러분의 건강을 책임지는 영양제 재고 관리 서비스 Pillin입니다!

m.onestore.co.kr

💡 Pillin 프로젝트의 백엔드 개발에서 사용한 아키텍처 및 규칙을 정리했습니다.

 

백엔드 구조도

 

규칙

  • Controller는 Dto만을 사용한다.
  • Service는 필요시 Vo, Entity에 저장된 데이터를 사용하여 연산한다.
    • Controller에서 전달받은 데이터 자체를 변환할 수 없게 하기 위해 Vo를 사용한다.
  • Dao는 Service로 받은 데이터로 DB에 정보를 조회한다.

 

요청 처리 Flow

  1. 사용자는 서버에 Rest하게 요청한다.
  2. Security Filter로 적합한 사용자가 요청한 것인지 판별한다.
    • 토큰이 필요하지 않으면 모든 요청이 적합하다고 판단
    • 토큰이 필요하면 토큰을 인증해서 적합한 사용자인지 판별
  3. 적합한 요청이면 요청에 맞는 Controller 메소드로 이동
    • 요청 정보는 ObjectMapper를 통해 Dto로 매핑됨
  4. Controller는 ControllerMapper를 통해 Service로 전송할 Vo 생성
  5. Service에 요청 위임
  6. Service는 전달받은 Vo를 사용하여 계산을 하거나, Dao에 정보를 요청
  7. Dao는 Jpa를 사용하여 DB에 정보 요청 후 반환
    • Jpa를 사용하면 일반적으로 요청받은 데이터는 Entity 클래스로 받을 수 있음(ORM)
  8. Service는 Dao를 통해 전달 받은 Entity 정보를 기반으로 나머지 연산 수행
  9. Service는 ServiceMapper를 통해 Controller에 반환할 Vo 생성 후 반환
  10. Controller는 응답 받은 결과를 사용자에게 반환

 

클래스 설명

0. Security Filter


0.1 특정 리소스로 들어오는 요청의 사용자 권한을 확인한다.

  • pill과 member의 하위 요청에 대해 사용자 권한(ADMIN, MEMBER)을 확인한다.
  • 그 이외의 요청은 권한이 필요 없다 → 로그인을 하지 않아도 요청 처리를 할 수 있따.
// SecurityConfig.java
.authorizeHttpRequests(authorize ->
                        authorize.requestMatchers("/api/v1/pill/**")
                                .access(new WebExpressionAuthorizationManager("hasRole('ADMIN') or hasRole('MEMBER')"))
                                .requestMatchers("/api/v1/member/**")
                                .access(new WebExpressionAuthorizationManager("hasRole('ADMIN') or hasRole('MEMBER')"))
                                .anyRequest().permitAll()
				               )

 

0.2 로그인에 성공하면 응답 헤더에 토큰을 담아서 전송한다.

  • 로그인에 성공하면, JWT를 생성해서 응답 헤더에 정보를 담는다.
// JwtAuthenticationFilter.java
@Override
protected void successfulAuthentication(HttpServletRequest request,
                                        HttpServletResponse response,
                                        FilterChain chain,
                                        Authentication authResult) {
    PrincipalDetails principalDetails = (PrincipalDetails) authResult.getPrincipal();
    String jwtAccessToken = jwtTokenManager.createAccessToken(principalDetails);
    response.addHeader(jwtProperties.getAccessTokenHeader(), jwtAccessToken);
}

 

1. Controller


1.1 REST 요청에 대해 응답한다.

  • REST하다는 것은 주로 json 또는 xml을 사용하는데, 이 프로젝트에서는 json을 사용했다.
// OwnPillController.java
@RestController
public class OwnPillController {}

 

1.2 엔드포인트는 같지만, HTTP 메소드를 달리해서 요청을 분기한다.

// OwnPillController.java
@GetMapping("/inventory")
@PostMapping("/inventory")
@PutMapping("/inventory")

 

1.3 Spring Security를 통해 요청 사용자 정보는 AuthenticationPrincipal 로 가져온다.

  • 사용자 정보는 PrincipalDetails에 담긴다.
// MemberController.java
info(@AuthenticationPrincipal PrincipalDetails principalDetails){}

 

1.4 Conroller에서 Service로 요청할 때, ControllerMapper를 이용해 Vo로 변환 후 요청한다.

// MemberController.java
memberService.register(mapper.mapToMemberRegisterVo(dto));

 

2. ControllerMapper


2.1 Service에서 사용될 Vo로 변환 후 반환한다.

// MemberControllerMapper.java
public MemberRegisterVo mapToMemberRegisterVo(RequestRegisterDto dto) {
    return MemberRegisterVo
        .builder()
        .email(dto.getEmail())
        .password(dto.getPassword())
        .name(dto.getName())
        .build();
}

 

3. Service


3.1 Service는 클래스 범위에서 트랜잭션을 읽기 전용으로 사용한다.

  • 읽기 요청에 대해 트랜잭션을 생성하지 않기 때문에 성능이 올라간다.
// MemberServiceImpl.java
@Transactional(readOnly = true)
public class MemberServiceImpl implements MemberService {}

 

3.2 수정 요청과 같이 트랜잭션이 필요할 때만 메소드에 트랜잭션을 추가한다.

  • 클래스 범위에서 읽기 전용으로 선언했기 때문에, 수정 요청에 대해서는 메소드 단위에서 선언이 필요하다.(우선순위는 클래스보다 메소드가 높다)
// MemberServiceImpl.java
@Transactional
public void register(MemberRegisterVo vo) {}

 

3.3 Entity 인스턴스에 작업이 필요할 때, 해당 Entity에 계산을 위임한다.

  • Entity를 도메인주도개발(DDD)의 도메인처럼 사용한다.
// OwnOwnPillServiceImpl.java
ownPill.runOutMessage()

 

4. ServiceMapper


4.1 Controller에서 사용될 Vo로 변환한다.

// OwnPillServiceMapper.java
public OutOwnPillInventorListVo mapToResponsePillInventorListVo(ResponsePillInventorListData takeTrue,
                                                                ResponsePillInventorListData takeFalse) {
    return OutOwnPillInventorListVo
        .builder()
        .takeTrue(takeTrue)
        .takeFalse(takeFalse)
        .build();
}

 

5. Dao


5.1 JpaRepository를 사용해서 DB에 정보를 조회 또는 수정한다.

// MemberDaoImpl.java
public void register(MemberProfile memberProfile) {
    memberJpaRepository.findByUsername(memberProfile.getUsername())
            .ifPresent(e -> {
                throw new IllegalArgumentException("이미 존재하는 이메일입니다.");
            });
    memberJpaRepository.save(memberProfile);
}

 

6. JpaRepository


6.1 JpaRepositroy를 구현해서 DB에 정보를 조회 또는 수정한다.

  • 상속을 받으면 실질적으로 SimpleJpaRepository가 동작한다.
// MemberJpaRepository.java
public interface MemberJpaRepository extends JpaRepository<MemberProfile, Long> {}

 

7. Entity


7.1 Jpa를 사용하기 위해 Entity를 만든다.

  • @Entity를 클래스 위에 선언하면 Jpa의 Entity로 사용할 수 있다.
// DeviceToken.java
@Entity
  • @Table(name = ”…”)를 클래스 위에 선언하면 해당 이름에 맞는 DB의 테이블에 매핑된다.
// DeviceToken.java
@Table(name = "devicetoken")
  • 생성자가 아닌 빌더 패턴으로만 인스턴스를 생성할 수 있다.
    • Jpa의 Entity를 쓰기 위해 생성자가 필요하지만, 외부에서는 생성자로 무분별한 인스턴스 생성을 하지 못하게 protected로 막았다.
// DeviceToken.java
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PROTECTED)