토이 프로젝트 스터디 #7
- 스터디 진행 날짜 : 6/11
- 스터디 작업 날짜 : 6/9 ~ 6/11
토이 프로젝트 진행 사항
docker-compose
사용
- 비즈니스 로직 개발
Account
관련 코드 작성
Custom ArgumentResolver
추가
내용
docker-compose
docker-compose
를 활용해 스프링 부트 애플리케이션을 도커로 관리할 수 있도록 설정
회원 권한 관련
@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_id | role_type
---------+------------
1 | ROLE_USER
2 | ROLE_ADMIN
account_id |
-----------+
1 |
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
에서 AccountId
와 Role
을 추출하는 내용을 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
를 받을 수 있도록 처리