토이 프로젝트 스터디 #7

appti·2022년 6월 11일
0

토이 프로젝트 스터디 #7

  • 스터디 진행 날짜 : 6/11
  • 스터디 작업 날짜 : 6/9 ~ 6/11

토이 프로젝트 진행 사항

  • docker-compose 사용
  • 비즈니스 로직 개발
    • Account 관련 코드 작성
    • Custom ArgumentResolver 추가

내용

docker-compose


회원 권한 관련

// Account

@Entity
public class Account {

    ...
    
    @OneToMany(mappedBy = "account", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<AccountRole> roles = new ArrayList<>();
    
    public void addRoles(List<Role> roles) {
        roles.stream().forEach(role -> {
            AccountRole accountRole = new AccountRole(this, role);
            this.roles.add(accountRole);
        });
    }

    public void addRole(Role role) {
        this.roles.add(new AccountRole(this, role));
    }
  • 해당 회원이 가지고 있는 권한에 따라 사용할 수 있는 게시판을 구분하고자 함
    • 이를 중간 관리자(ex : 네이버 카페 매니저)가 권한을 관리할 수 있도록 설정하고자 함
    • 기존 단순 enum으로 관리하던 권한과 해당 회원이 가지고 있는 권한의 조합을 DB에서 관리해야 할 필요성을 인지
    • 단순 회원 ID권한 ID를 저장할 것이기 때문에 PK를 복합 키로 관리하고자 함
@Getter
@Embeddable
@EqualsAndHashCode
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class AccountRoleId implements Serializable {

    private Account account;
    private Role role;
}

@Entity
@Getter
@IdClass(AccountRoleId.class)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@EqualsAndHashCode
public class AccountRole {

    @Id
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "account_id")
    private Account account;

    @Id
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "role_id")
    private Role role;
}
  • @Embadded 방식과 @IdClass 방식 중 @IdClass 방식 사용
# role
role_id | role_type
---------+------------
       1 | ROLE_USER
       2 | ROLE_ADMIN

# account

account_id | 
-----------+
         1 |

# account_role
 account_id | role_id
------------+---------
          1 |       1
  • 회원 가입 직후 DB를 확인해보면 위와 같이 기본적인 권한(ROLE_USER)를 가지고 있음을 확인할 수 있음

회원 탈퇴 관련

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/accounts")
public class AccountController {

    ...
    
        @PostMapping("/withdrawal")
    public ResponseEntity accountWithdrawal(@LoginAccount AccountInfo accountInfo) {
        accountService.withdrawalAccount(accountInfo);
        return new ResponseEntity(HttpStatus.OK);
    }

    @PostMapping("/withdrawal/cancel")
    public ResponseEntity cancelWithdrawalAccount(@LoginAccount AccountInfo accountInfo) {
        accountService.cancelWithdrawalAccount(accountInfo);
        return new ResponseEntity(HttpStatus.OK);
    }
}
  • 회원 탈퇴 시 즉시 탈퇴가 아닌 일정 시간이 지난 뒤 탈퇴하도록 설정
    • 회원 탈퇴(/withdrawal)과 회원 탈퇴 취소(/withdrawal/cancel) api 생성
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class AccountService {

    private final AccountSchedulerUtils accountSchedulerUtils;
    
    ...
    
    @Transactional
    public void withdrawalAccount(AccountInfo accountInfo) {
        Account account = accountRepository.findById(accountInfo.getAccountId()).orElseThrow(AccountNotFoundException::new);
        account.withdrawalAccount();
        accountSchedulerUtils.initAccountWithdrawalJob(accountInfo.getAccountId(), accountRepository);
    }

    @Transactional
    public void cancelWithdrawalAccount(AccountInfo accountInfo) {
        Account account = accountRepository.findById(accountInfo.getAccountId()).orElseThrow(AccountNotFoundException::new);
        account.cancelWithdrawalAccount();
        accountSchedulerUtils.deleteAccountWithdrawalJob(accountInfo.getAccountId());
    }
}
  • 기존 회원가입 때 사용했던 quartz를 활용
    • quartz에 의존적인 코드는 AccountSchedulerUtils로 분리

Custom ArgumentResolver

  • 헤더로 전송되는 Access Token에서 AccountIdRole을 추출하는 내용을 Custom ArgumentResolover로 처리
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginAccount {
}

@Getter
@AllArgsConstructor
public class AccountInfo {

    private Long accountId;

    private List<String> roleTypes;
}


@RequiredArgsConstructor
public class LoginAccountArgumentResolver implements HandlerMethodArgumentResolver {

    private final TokenHelper accessTokenHelper;

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.getParameterAnnotation(LoginAccount.class) != null
                && parameter.getParameterType().equals(AccountInfo.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        String authorizationHeader = webRequest.getHeader("Authorization");

        if (authorizationHeader == null) {
            return new AccountInfo(null, null);
        }

        PrivateClaims privateClaims =
                accessTokenHelper.parse(authorizationHeader).orElseThrow(TokenFailureException::new);
        Long accountId = Long.valueOf(privateClaims.getAccountId());
        List<String> roleTypes = privateClaims.getRoleTypes();
        return new AccountInfo(accountId, roleTypes);
    }
}

@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {

    ...
    
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(loginAccountArgumentResolver(accessTokenHelper));
    }

    @Bean
    public LoginAccountArgumentResolver loginAccountArgumentResolver(TokenHelper accessTokenHelper) {
        return new LoginAccountArgumentResolver(accessTokenHelper);
    }
}
  • 컨트롤러에서 @LoginAccount를 통해 바로 AccountInfo를 받을 수 있도록 처리
profile
안녕하세요

0개의 댓글