https://m.onestore.co.kr/mobilepoc/apps/appsDetail.omp?prodId=0000774295
💡 Pillin 프로젝트의 백엔드 개발에서 사용한 아키텍처 및 규칙을 정리했습니다.
백엔드 구조도
규칙
- Controller는 Dto만을 사용한다.
- Service는 필요시 Vo, Entity에 저장된 데이터를 사용하여 연산한다.
- Controller에서 전달받은 데이터 자체를 변환할 수 없게 하기 위해 Vo를 사용한다.
- Dao는 Service로 받은 데이터로 DB에 정보를 조회한다.
요청 처리 Flow
- 사용자는 서버에 Rest하게 요청한다.
- Security Filter로 적합한 사용자가 요청한 것인지 판별한다.
- 토큰이 필요하지 않으면 모든 요청이 적합하다고 판단
- 토큰이 필요하면 토큰을 인증해서 적합한 사용자인지 판별
- 적합한 요청이면 요청에 맞는 Controller 메소드로 이동
- 요청 정보는 ObjectMapper를 통해 Dto로 매핑됨
- Controller는 ControllerMapper를 통해 Service로 전송할 Vo 생성
- Service에 요청 위임
- Service는 전달받은 Vo를 사용하여 계산을 하거나, Dao에 정보를 요청
- Dao는 Jpa를 사용하여 DB에 정보 요청 후 반환
- Jpa를 사용하면 일반적으로 요청받은 데이터는 Entity 클래스로 받을 수 있음(ORM)
- Service는 Dao를 통해 전달 받은 Entity 정보를 기반으로 나머지 연산 수행
- Service는 ServiceMapper를 통해 Controller에 반환할 Vo 생성 후 반환
- 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)
'삽질' 카테고리의 다른 글
@EnableBatchProcessing 사용시 설정 Back Off (0) | 2024.05.04 |
---|---|
UnsupportedProductError The client noticed that the server is not a supported distribution of Elasticsearch (2) | 2024.03.14 |
HttpUrlConnection, RestTemplate, WebClient의 특징과 장단점 (0) | 2023.12.31 |
오픈소소는 공짜가 아니다. (0) | 2023.11.29 |
헥사고날 아키텍처 (0) | 2023.11.28 |