public class Member extends Auditable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "email", unique = true, nullable = false, length = 50)
private String email;
@Column(name = "nickname", unique = true, nullable = false, length = 50)
private String nickName;
@Column(name = "password", nullable = false)
private String password;
@Column(name = "country", nullable = false, length = 50)
private String country;
@Column(name = "status", nullable = false)
@Enumerated(value = EnumType.STRING)
private MembershipStatus status;
@Column
@Temporal(TemporalType.TIMESTAMP)
private LocalDateTime deletedAt;
}
private void isDuplicateMember(MemberSignUpCommand command) {
// 이메일 중복 확인
Optional<Member> foundByEmail = memberRepository.findByEmail(command.getEmail());
if (foundByEmail.isPresent()) {
throw new CustomException(ALREADY_EMAIL);
}
// 닉네임 중복 확인
Optional<Member> foundByNickName = memberRepository.findByNickName(command.getNickName());
if (foundByNickName.isPresent()) {
throw new CustomException(ALREADY_NICKNAME);
}
}
문제 발생 : 현재 규모의 프로젝트에선 Email 과 NickName 을 unique 값으로 두어 중복문제를 해결하였음. 유저의 입력값의 중복검증을 하기위해 2번의 쿼리를 날려야하는 상황이 발생.
원인 : 각각의 입력요소에 대해서 2번의 DB접속의 요구가 필요한 코드가 작성되어짐.
가설 : 굳이 2개의 요소가 모두 중복일 필요없이 Email 혹은 NickName 이 중복되는 Row 가 단 1개라도 존재하면 어차피 불가능한게 아닐까?
문제해결 : 문제의 코드를 리팩토링 하여 findByEmailOrNickName 쿼리메소드를 사용 그 후 찾아온 Member 객체가 존재한다면 사용이 불가능한 Email 혹은 NickName 이기에 예외를 발생시키도록 해결
private void isDuplicateMember(MemberSignUpCommand command) {
Optional<Member> foundMember = memberRepository.findByEmailOrNickName(
command.getEmail(), command.getNickName()
);
if (foundMember.isPresent()) {
Member existingMember = foundMember.get();
if (existingMember.getEmail().equals(command.getEmail())) {
throw new CustomException(ALREADY_EMAIL);
}
if (existingMember.getNickName().equals(command.getNickName())) {
throw new CustomException(ALREADY_NICKNAME);
}
}
}
existsByNickName 같은) 더 설명적인 코드와 간결성을 유지 할 수 있을거라고 생각, @Column(name = "country", nullable = false, length = 50)
private String country;
문제 발생 : 유저의 Entity 값으로 거주 국가의 대한 데이터가 들어가야하는 설계구조 에서 국가가 실존하는 국가인지 검증하는 작업이 필요.
원인 : 유저의 Entity 값으로 거주 국가의 대한 데이터가 들어가야하는 설계구조
가설 :

문제해결 :
@PostConstruct
public void initCountryNames() {
Object[] countries = restTemplate.getForObject(API_URL, Object[].class);
if (countries != null) {
countryNames = Arrays.stream(countries)
.map(country -> (Map<String, Object>) country)
.map(countryMap -> (Map<String, String>) countryMap.get("name"))
.map(nameMap -> nameMap.get("common"))
.toList();
}
}
[South Georgia, Grenada, Switzerland, Sierra Leone, Hungary, Taiwan,
Wallis and Futuna, Barbados, Pitcairn Islands, Ivory Coast, Tunisia,
Italy, Benin, Indonesia, Cape Verde, Saint Kitts and Nevis, ...]
@NotBlank(message = "거주 국가를 입력해주세요.")
@Country(message = "존재하지 않는 국가 이거나 국가명을 영문으로 입력해주세요.")
private String country;
@Valid 를 지원하지만 나라에대한 검증은 기존 라이브러리로 제공하지않음. 그렇다고 서비스에서 검증까지 갈 문제는 아니라 판단. 예외처리 처럼 내가 하나 만들면되지 않을까?@RequiredArgsConstructor
public class CountryValidator implements ConstraintValidator<Country, String> {
private final CountryService countryService;
@Override
public void initialize(Country constraintAnnotation) {
}
@Override
public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
return countryService.getCountryNames().contains(value);
}
}
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = CountryValidator.class)
public @interface Country {
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String message();
}
ConstraintValidator 의 구현체와 사용될 어노테이션 클래스를 생성하여 해당 Validator의 메소드를 오버라이딩하여 검증로직을 구현.@Target : 어노테이션을 적용하는 위치를 지정, 필드 혹은 파라미터에서 사용가능하도록 설정.@Retention : 어노테이션의 유지범위를 지정. 서버가 실행되는 동안 으로 설정.@Constratin : 해당 필드값을 검증할 검증클래스를 지정한다.
참조 : https://wildeveloperetrain.tistory.com/157

문제 발생 : 프로젝트를 진행하면서 로그인 인증처리가 필요.
원인 : 대부분의 API 서비스에서 로그인 등의 인증절차를 통해야만 하는 기능들이 존재
가설 : 기존의 JWT필터만 사용하는게 아니라 Spring Security 의 기반을 활용하여 개발이 불가능 할까? 강의와 레퍼런스들에서는 Spring Security 만을 사용하는게 아니라 혼합해서 사용하는것이 일반적이라고 판단.
문제 해결 :
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurityConfig {
private final JwtUtil jwtUtil;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf((csrf) -> csrf.disable())
.formLogin((csrf) -> csrf.disable())
.httpBasic((csrf) -> csrf.disable())
.authorizeRequests((authorizeRequests) ->
authorizeRequests
.requestMatchers("/api/member/signup", "/api/member/login").permitAll()
.anyRequest().authenticated()
);
http.sessionManagement(sessionManagement ->
sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
http.addFilterBefore(authenticationExceptionHandlerFilter(), UsernamePasswordAuthenticationFilter.class);
http.addFilterBefore(authFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public AuthFilter authFilter() {
return new AuthFilter(jwtUtil);
}
@Bean
public AuthenticationExceptionHandlerFilter authenticationExceptionHandlerFilter() {
return new AuthenticationExceptionHandlerFilter();
}
securityFilterChain 에서 JWT 기능을 사용하기위해 기존의 폼로그인 화면과 세션방식을 OFF 하고 http.addFilterBefor 를 이용하여 구현한 로그인 filer 를 붙여주는 형태로 public Authentication getAuthentication(String token) {
Claims claims = getUserInfoFromToken(token);
String email = claims.getSubject();
Member member = memberRepository.findByEmail(email).orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));
List<SimpleGrantedAuthority> authorities = Collections.emptyList(); // 권한이 필요 없다면 빈 리스트
return new UsernamePasswordAuthenticationToken(member, token, authorities);
}
public class AuthFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws ServletException, IOException {
if (isFilterApplicable(req)) {
chain.doFilter(req, res);
return;
}
String tokenValue = jwtUtil.getTokenFromRequest(req);
if (!StringUtils.hasText(tokenValue)) {
throw new UnAuthorizationException(TOKEN_NOT_FOUND);
}
String accessToken = jwtUtil.substringToken(tokenValue);
if (!jwtUtil.validateToken(accessToken)) {
throw new UnAuthorizationException(INVALID_TOKEN);
}
// JWT 로부터 인증 정보를 가져옴
Authentication auth = jwtUtil.getAuthentication(accessToken);
// 인증 정보를 설정
SecurityContextHolder.getContext().setAuthentication(auth);
chain.doFilter(req, res);
}
private boolean isFilterApplicable(HttpServletRequest req) {
String path = req.getRequestURI();
return path.startsWith("/api/member/signup") || path.startsWith("/api/member/login");
}
}
Authentication auth = jwtUtil.getAuthentication(accessToken); 와 SecurityContextHolder.getContext().setAuthentication(auth); 부분이다Authentication : 사용자의 인증정보를 저장하는 토큰의 개념으로 현재의 구조로는 이미 검증이 완료된 토큰이기에 사용자정보를 담기위해 사용했다.SecurityContextHolder : 유저의 정보가 들어있는 UserDetails 를 담고있으며, 인증이 완료된 auth 정보를 넣어줬다.인가 의 대한 요구처리가 없기에 Spring Security 를 크게 활용할 점은 없음. 다만 요구조건이 추가됄 경우 활용성이 높기에 확장성이 높다고 생각한다.UsernamePasswordAuthenticationFilter 를 구현하여 인증 처리를 하는것 같음. 하지만 폼로그인 방식을 사용하지도 않았고 OncePerRequestFilter 를 사용할 경우 인증처리에 대해서 좀 더 개발자가 커스텀을 할수 있는 장점이 있음.
문제 발생 : 유저가 해당 NewsFeed 프로그램을 탈퇴시 유저의 Email 을 보존해야하는 상황 발생.
원인 : 프로그램의 요구조건으로 탈퇴한 유저의 Email을 사용하지 못하도록 제한되어있음.
가설 : 해당 요구조건을 만족시킬 수 있는 2가지의 방법이 생각났음.
@Transactional
public MemberDeleteResponseDto deleteMember(Long id, MemberDeleteRequestDto req) {
Member member = memberRepository.findById(id).orElseThrow(() -> new CustomException(USER_NOT_FOUND));
if (!member.isValidPassword(req.getPassword(), passwordEncoder)) {
throw new CustomException(LOGIN_FAILED);
}
member.anonymizeMember();
memberRepository.save(member);
likeRepository.deleteByMemberId(member.getId());
newsRepository.deleteByMemberId(member.getId());
friendRepository.deleteAllByMemberOrFriend(member.getId());
friendRequestRepository.deleteByReceiverIdOrRequesterId(member.getId());
return MemberDeleteResponseDto
.builder()
.id(member.getId())
.message("회원탈퇴 되었습니다.")
.build();
}
해당 프로그램의 서비스기간이 길어질수록 사용불가능한 유저이름 + Email 의 경우가 많아진다. 이를 해결하려면 Spring Batch 를 활용하여 일정기간이 지난 삭제유저를 HardDelete 로 해결가능해보임.
유저가 자신을 복구시 자신의 유저이름을 가진 ROW 가 존재할시 중복현상이 발생. 유저정보를 수정할수있도록 유도하면 괜찮을것 같음.

문제 발생 : 협업으로 Git Hub를 통해 코드를 공유하면서 최대한 충돌과 코드오염을 적게 하기위한 방법이 필요.
원인 : GitHub 내에서 코드를 공유히면서 같은줄에 다른코드가 있거나 했을때의 충돌발생. 범위가 커서 Table 별로 인원을 분배하는게 가장 베스트같지만 그렇게 규모가 큰 프로젝트가 아니었음.
가설 : GitHub 자체는 매우 대중적인 코드공유저장소로 많은 전략이나 응용프로그램이 있을거라 판단.
문제 해결 : 예상대로 Git으로 협업을위한 많은 전략이 있었고 그중 Git Flow 전략을 사용하기로 결정. 트리구조의 형태로 개인의 기능브랜치를 이슈번호로 매핑하여 하나씩 합치는 전략을 사용했음.
Git 트리구조 예시
---------------------------------------------------------------------------------
Main
ㄴ
dev
ㄴ
Member
ㄴ feat - 1 (회원탈퇴)
ㄴ feat - 27(친구요청)
News
ㄴ feat - 5 (게시물생성)
Comment
ㄴ feat - 8 (댓글생성)

