Spring 7기 프로젝트/뉴스피드 팀 프로젝트
[트러블슈팅] 프로젝트 개발 중 발생한 3가지 핵심 기술적 이슈에 대한 팀 차원의 결정사항
JuNo_12
2025. 5. 30. 16:11
회의 1: 게시물 수정/삭제 권한 제어 방식
검토된 방안들
- 방안 1: Service Layer에서 직접 검증
- 방안 2: Spring Security @PreAuthorize 활용
- 방안 3: Repository Query 필터링
결정사항: 도메인별 유틸리티 클래스를 활용한 권한 검증
// PostAuthorizationUtil.java (게시물 전용)
@Component
public class PostAuthorizationUtil {
public Post validateUserAccessToPost(Long postId, UserDetails userDetails,
PostRepository postRepository,
UserRepository userRepository) {
User foundUser = userRepository.findByEmail(userDetails.getUsername())
.orElseThrow(() -> new UserNotFoundException("사용자를 찾을 수 없습니다."));
Post foundPost = postRepository.findById(postId)
.orElseThrow(() -> new PostNotFoundException("게시물을 찾을 수 없습니다."));
if(!foundUser.getId().equals(foundPost.getUser().getId())) {
throw new UserMismatchException("내가 작성하지 않은 게시물은 수정할 수 없습니다.");
}
return foundPost;
}
}
// CommentAuthorizationUtil.java (댓글 전용)
@Component
public class CommentAuthorizationUtil {
public Comment validateUserAccessToComment(Long commentId, UserDetails userDetails,
CommentRepository commentRepository,
UserRepository userRepository) {
User foundUser = userRepository.findByEmail(userDetails.getUsername())
.orElseThrow(() -> new UserNotFoundException("사용자를 찾을 수 없습니다."));
Comment foundComment = commentRepository.findById(commentId)
.orElseThrow(() -> new CommentNotFoundException("댓글을 찾을 수 없습니다."));
// 댓글 작성자 또는 게시물 작성자만 삭제 가능
if (!foundComment.getUser().getId().equals(foundUser.getId())
&& !foundComment.getPost().getUser().getId().equals(foundUser.getId())) {
throw new UserMismatchException("댓글 삭제 권한이 없습니다.");
}
return foundComment;
}
}
결정 이유
- 도메인별 특화: 각 서비스마다 필요한 권한 검증 로직이 다름
- 게시물: 작성자만 수정/삭제 가능
- 댓글: 댓글 작성자 또는 게시물 작성자가 삭제 가능
- 구조적 동일성: 유틸리티 클래스 방식과 @PreAuthorize 방식이 구조적으로 거의 동일
- 팀 역량 고려: @PreAuthorize의 SpEL, AOP 복잡성 대비 팀 이해도 높음
- 점진적 개선: 기존 코드에서 단계적 리팩토링 가능
회의 2: 상위 데이터 삭제 시 하위 데이터 처리 방식
검토된 방안들
- 방안 A: CASCADE 삭제 (자동 삭제)
- 방안 B: Soft Delete (논리적 삭제)
- 방안 C: 별도 테이블로 이동
결정사항: Soft Delete (논리적 삭제) 방식
// BaseEntity.java
@MappedSuperclass
public abstract class BaseEntity {
@CreatedDate
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime modifiedAt;
private LocalDateTime deletedAt; // 추가
@Column(nullable = false)
private Boolean deleted = false; // 추가
}
추가 논의: 서비스 책임 분리
검토된 방안들:
- 방안 1: 유저 서비스에서 관련 댓글과 게시물을 한번에 처리
- 방안 2: 각 서비스별 책임 분리 + 상위에서 조합
최종 결정: 방안 2 (책임 분리 방식)
// UserService.java
@Transactional
public void deleteUser(Long userId) {
// 1. 댓글 논리적 삭제
commentService.deleteCommentsByUserId(userId);
// 2. 게시물 논리적 삭제
postService.deletePostsByUserId(userId);
// 3. 사용자 논리적 삭제
userRepository.softDelete(userId);
}
결정 이유
- 데이터 복구 가능성: 사용자 실수나 시스템 오류 대응
- 책임 분리 원칙: 각 서비스가 자신의 도메인만 담당
- 확장성: 향후 복잡한 비즈니스 로직 추가 용이
회의 3: Service Layer와 인증 객체 의존성
검토된 방안들
- 방안 A: 현재 방식 유지 (UserDetails 직접 사용)
- 방안 B: CustomUserDetails 활용 개선
- 방안 C: 인터셉터를 활용한 자동 주입
- 방안 D: Controller에서 직접 추출
결정사항: 방안 B (CustomUserDetails 활용 개선)
// 기존 방식 (성능 이슈)
public PostResponseDto createPost(UserDetails userDetails, CreatePostRequestDto dto) {
User user = userRepository.findByEmail(userDetails.getUsername()) // 매번 DB 조회
.orElseThrow(() -> new UserNotFoundException("사용자를 찾을 수 없습니다."));
}
// 개선된 방식
public PostResponseDto createPost(CustomUserDetails userDetails, CreatePostRequestDto dto) {
Long userId = userDetails.getUserId(); // DB 조회 없이 바로 사용
User user = userRepository.findById(userId)...
}
결정 이유
- 성능 개선: 불필요한 email 기반 DB 조회 제거
- 점진적 개선: 기존 구조 최소 변경으로 개선 효과 획득
- 팀 부담 최소: UserDetails → CustomUserDetails 타입 변경만 필요
- 실용성: 이론적 완벽함보다 현실적 개발 효율성 우선
전체 결정사항 요약
| 구분 | 결정사항핵심 | 이유 |
| 권한 제어 | 도메인별 유틸리티 클래스 | 도메인 특화 + 팀 이해도 |
| 데이터 삭제 | Soft Delete + 책임 분리 | 복구 가능성 + 확장성 |
| 인증 객체 | CustomUserDetails 개선 | 성능 + 최소 변경 |
공통 결정 원칙
- 팀 역량 고려: 복잡한 기술보다 팀이 이해하고 유지보수할 수 있는 방식 선택
- 점진적 개선: 기존 코드 구조를 최대한 활용하면서 단계적 개선
- 실용성 우선: 이론적 완벽함보다 현실적 개발 효율성과 일정 준수