📝느낀 점
이번 프로젝트를 통해 라이브러리 사용법과 데이터베이스 트랜잭션 관리의 중요성을 알게 되었습니다. 특히 트랜잭션 관리를 소홀히 할 경우 생각지 못한 오류가 발생할 수 있음을 알게 되었습니다. 또한, 데이터베이스의 외래키 제약 조건을 유지하면서 논리적 삭제를 처리하는 방법을 배우며, @SQLDelete와 @Where 어노테이션의 유용성을 깨달았습니다. 사용자 입력 데이터의 유효성을 철저히 검증하는 것이 시스템의 안정성을 위해 얼마나 중요한지 알게 되었습니다. 이러한 경험을 바탕으로 유사한 문제를 더 효과적으로 해결할 수 있는 경험을 하게 되었습니다. 이번 경험은 앞으로의 개발에 도움이 될 것이라고 생각합니다.
어떻게 했기에 문제상황을 마주하게 되었는지
회원 탈퇴 기능을 구현하는 과정에서 RESTful API 설계를 준수하기 위해
@DeleteMapping을 사용하여 회원 탈퇴 요청을 처리했습니다.
클라이언트 측에서는 비밀번호를 포함한 데이터를 DELETE 메소드로 서버에 전송하려 했으나,
비밀번호와 같은 민감한 데이터를 URL에 포함시키는 것은 보안상 위험하다는 문제가 발생했습니다.
그리하여, DELETE 요청의 본문을 처리하지 않는 서버 구현과 쿼리 파라미터로 데이터를 받는 어려움을 겪었습니다.
또한, 논리적 삭제를 위해 데이터베이스에서 deleted 컬럼을 사용했지만, email 컬럼의 유니크 제약 조건으로 인해
논리적으로 삭제된 회원의 이메일과 동일한 이메일로 새로운 회원을 가입시키려 할 때 예외가 발생했습니다.
이는 데이터베이스에서 논리적으로 삭제된 레코드도 중복 체크에서 제외되지 않았기 때문입니다.
이게 왜 문제인지
RESTful API 설계 원칙에 따르면, DELETE 메소드는 리소스 삭제를 요청할 때 주로 사용됩니다.
일반적으로 URL 경로에 필요한 데이터를 포함시키지만, 비밀번호와 같은
민감한 데이터를 URL에 포함시키는 것은 보안상 큰 위험이 있을수도 있습니다.
예를 들어, URL에 포함된 비밀번호는 로그나 브라우저 히스토리에
저장될 수 있어 악의적인 사용자에게 노출될 위험이 있습니다.
또한, 일부 서버 구현에서는 DELETE 요청의 본문을 처리하지 않거나
쿼리 파라미터로 데이터를 받는 데 제약이 있을 수 있습니다.
논리적으로 삭제된 레코드의 경우, 데이터베이스의 유니크 제약 조건이
여전히 적용되어 동일한 이메일로 새로운 회원을 가입시키려 할 때 예외가 발생합니다.
그렇기에, 데이터베이스에서 논리적 삭제를 처리할 때 발생할 수 있는
중복 체크 문제로, 데이터 무결성을 유지하기 어렵게 만든다고 생각했습니다.
문제를 어떻게 감지했는지
클라이언트 측에서 DELETE 요청을 보낼 때, 서버가 비밀번호를
제대로 인식하지 못하고 요청이 예상대로 처리되지 않는 것을 확인했습니다.
이는 클라이언트와 서버 간의 상호작용 문제를 나타냅니다.
또한, 비밀번호를 URL에 포함시키는 방식이 보안상 적절하지 않다는 것을 인지하게 되었습니다.
회원 가입 시 "Duplicate entry" 예외 메시지를 통해 논리적으로
삭제된 레코드가 여전히 중복 체크에 포함되고 있음을 알게 되었습니다.
이는 데이터베이스 로그와 애플리케이션 로그를 분석하여 문제를 감지하는 데 중요한 역할을 하게 했습니다.
이러한 로그를 통해 데이터베이스의 유니크 제약 조건이 삭제된 레코드에도 적용되고 있음을 확인할 수 있었습니다.
어떻게 해결했는지
비밀번호와 같은 민감한 데이터를 URL에 포함시키지 않도록 클라이언트 측에서 DELETE 요청을 보낼 때,
비밀번호를 요청 본문에 포함하여 전송하도록 변경했습니다.
서버 측에서는 @DeleteMapping과 @RequestParam을 함께 사용하는 대신
@RequestBody를 통해 데이터를 받도록 수정했습니다.
이를 통해 DELETE 요청의 보안 문제를 해결할 수 있었습니다.
@DeleteMapping
public ResponseEntity<MemberDeletedResponse> softDeleteMember(@Validated @RequestBody MemberDeleteRequest deleteRequest) {
MemberDeleteRequestDTO memberDeleteRequestDTO = MemberDeleteRequest.toMemberDeleteRequestCommand(deleteRequest);
boolean softDeleteMember = memberFacade.softDeleteMember(memberDeleteRequestDTO);
MemberDeletedResponse response = MemberDeletedResponse.toSoftDeleteMemberResponse(softDeleteMember);
return ResponseEntity.ok(response);
}
클라이언트 측에서는 AJAX 요청을 통해 DELETE 요청을 보내며, 비밀번호를 JSON 형식의 본문에 포함시켰습니다.
function deleteMember(event) {
event.preventDefault();
var password = $('#password').val();
$.ajax({
url: "/api/v1/member",
type: 'DELETE',
contentType: 'application/json',
data: JSON.stringify({password: password}),
headers: {
'Authorization': 'Bearer ' + localStorage.getItem("jwt")
},
success: function(memberDeleteResponse) {
if (memberDeleteResponse.success) {
alert(memberDeleteResponse.message);
localStorage.removeItem('jwt');
window.location.href = "/";
} else {
alert(memberDeleteResponse.message);
}
},
error: function(xhr, status, error) {
alert('회원 비밀번호가 일치하지 않습니다.');
}
});
}
논리적 삭제 문제를 해결하기 위해 엔티티 클래스에 @SQLDelete 및 @Where 어노테이션을 추가하고,
중복 체크 메서드를 수정하여 논리적으로 삭제된 레코드를 제외하고 중복을 확인하도록 했습니다.
@Entity
@Getter
@Builder
@EqualsAndHashCode(of = "id")
@AllArgsConstructor
@SQLDelete(sql = "update member set deleted = true where id = ?")
@Where(clause = "deleted = false")
public class Member extends AuditingFields implements Serializable, UserDetails {
@Column(name = "deleted")
private boolean deleted = Boolean.FALSE;
...
}
public interface MemberRepository extends JpaRepository<Member, Long> {
@Query("SELECT COUNT(m) FROM Member m WHERE m.email = :email AND m.deleted = false")
Long countByEmailIgnoringDeleted(@Param("email") String email);
}
서비스 레이어에서는 중복 체크 로직을 추가하여 회원 가입 전에 중복을 확인하도록 했습니다.
@Service
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
private final PasswordEncoder passwordEncoder;
public Member joinMember(MemberSaveRequestDTO memberDTO) {
checkForDuplicate(memberDTO);
Member memberEntity = Member.builder()
.name(memberDTO.getName())
.passwordHash(passwordEncoder.encode(memberDTO.getPassword()))
.email(memberDTO.getEmail())
.nickname(memberDTO.getNickname())
.role(memberDTO.getRole())
.build();
return memberRepository.save(memberEntity);
}
private void checkForDuplicate(MemberSaveRequestDTO memberDTO) {
if (memberRepository.existsByNicknameAndDeletedFalse(memberDTO.getNickname())) {
throw new DuplicateNicknameException("닉네임이 이미 사용 중입니다.");
}
if (memberRepository.countByEmailIgnoringDeleted(memberDTO.getEmail()) > 0) {
throw new DuplicateEmailException("중복된 이메일 주소입니다.");
}
}
}
그렇게 하면 왜 해결 되는지
비밀번호와 같은 민감한 정보를 요청 본문에 포함시켜 전송하면
서버 측에서 @RequestBody를 통해 데이터를 쉽게 받을 수 있습니다.
이는 URL에 민감한 정보를 포함시키는 것보다 보안상 안전하다고 생각합니다.
논리적 삭제의 경우, @SQLDelete 및 @Where 어노테이션을 사용하여 삭제된 레코드를 처리하고, 중복 체크 로직을
통해 논리적으로 삭제된 레코드를 제외하여 데이터베이스의 유니크 제약 조건 문제를 해결할 수 있습니다.
이러한 접근 방식은 보안성과 데이터 무결성을 동시에 향상시킵니다.
서버와 클라이언트 간의 데이터 전송이 괜찮아지면, 민감한 정보를 안전하게 전송할 수 있습니다.
또한, 데이터베이스의 논리적 삭제 처리가 정확하게 이루어지며,
중복 문제를 해결하여 데이터 일관성을 유지할 수 있습니다.
얼마나 개선되었는지
이 방법을 통해 클라이언트와 서버 간의 데이터 전송이 원활하게 이루어졌으며, 요청이 성공적으로 처리되었습니다.
특히 보안상 위험도를 줄여 비밀번호 등의 민감한 정보를 안전하게 전송할 수 있게 되었습니다.
또한, 논리적으로 삭제된 레코드를 중복 체크에서 제외함으로써
데이터베이스의 유니크 제약 조건 문제를 해결할 수 있었습니다.
서버와 클라이언트 간의 상호작용이 원활해지면서 시스템의 안정성이 크게 올라갔습니다.
데이터 전송의 안전성이 보장되었고, 논리적 삭제 문제도 해결되었습니다.
시스템의 보안과 데이터 일관성이 향상되면서 사용자 만족도도 높아졌습니다.
배우는 것은 무엇인지
DELETE 메소드와 데이터 전송 방법에 대해 더 깊이 이해하게 되었습니다.
민감한 데이터를 URL에 포함시키는 것이 얼마나 위험한지 깨달았습니다.
논리적 삭제를 적용할 때 추가적인 로직과 복잡성을 이해하고,
이를 효과적으로 처리하는 방법을 배웠습니다.
Spring Data JPA의 기능을 활용하여 복잡한 데이터 처리 로직도
비교적 간단하게 구현할 수 있다는 것을 알게 되었습니다.
또한, RESTful API 설계 원칙과 보안 고려사항을 학습하면서
더 안전한 웹 애플리케이션을 개발할 수 있는 능력을 키웠습니다.
이번 경험을 통해 DELETE 메소드의 적절한 사용법과 민감한 데이터 전송 시의 보안 고려사항을 학습했습니다.
또한, 논리적 삭제 처리와 관련된 복잡성을 이해 했으며, 그 해결하는 능력을 키울 수 있었습니다.
Spring Data JPA의 기능을 활용하여 효율적인 데이터 처리를 구현하는 방법을 배웠습니다.
RESTful API 설계 원칙을 준수하면서도 보안을 생각하는 애플리케이션 개발의 중요성도 알게 되었습니다.
무엇을 얻을 것인지
이번 문제 해결을 통해 더 효율적이고 유지보수성이 높은 코드를 작성하는 경험을 얻었습니다.
Spring Security와 Hibernate의 다양한 기능을 활용하면서 보안과 성능을 동시에 고려한 시스템 설계 방법을 배웠습니다.
이러한 경험을 통해 앞으로의 프로젝트에서도 유사한 문제를 효과적으로 해결할 수 있는 경험을 얻었다고 생각하며,
지속적인 학습과 개선을 통해 더 나은 웹 애플리케이션을 개발하는 데 도움이 될 것입니다.
이 방법이 최선이었는지
JPA의 cascade 옵션을 활용한 방식은 JPA의 표준적인 방법이며,
성능과 유지보수성을 고려할 때 효율적인 방법입니다.
상황에 따라 직접 쿼리를 작성하거나 다른 프레임워크를 사용하는 방법도 있지만,
JPA의 기본 기능을 최대한 활용하는 것이 좋다고 생각합니다.
Spring Security와 JWT를 사용하여 인증과 권한 부여를 관리하는 것은
많이 사용되는 표준적인 방식이며, 특히 확장성과 보안 측면에서 많은 장점을 제공합니다.
다른 방법은 없었는지
다른 방법으로는 애플리케이션 코드에서 수동으로 연관된 엔티티를 삭제하는 로직을 추가하는 것이 있습니다.
예를 들어, 회원 탈퇴 요청을 처리할 때 해당 회원의 모든 연관 데이터를 명시적으로 삭제하는 것입니다.
이 접근 방식은 코드 복잡성을 증가시키고, 유지보수가 어렵다는 단점이 있습니다.
데이터베이스 트리거를 사용하여 부모 테이블의 행이 삭제될 때
관련된 행을 자동으로 삭제하도록 설정할 수도 있습니다.
그러나 이 방법은 데이터베이스 의존성을 높이고 JPA의 장점을 충분히 활용하지 못할 수 있습니다.
또한, Spring Security를 사용하여 비밀번호 검증과 탈퇴 로직을 더 강화하는 방법이나,
데이터베이스 트랜잭션을 더욱 세밀하게 관리하는 방법도 고려할 수 있습니다.
하지만 현재 구현한 방법이 성능과 유지보수성 측면에서 가장 적합하다고 생각했습니다.
'프로젝트(project)' 카테고리의 다른 글
비밀번호 검증 및 데이터 무결성 유지 (0) | 2024.07.25 |
---|---|
비밀번호 해싱과 Security로 보안 높이기 (0) | 2024.07.25 |
NonUniqueResultException 해결하기 (0) | 2024.07.24 |
비밀번호 찾기 및 재설정과 임시 비밀번호 문제 (0) | 2024.07.24 |
MySQL과 S3를 활용한 효율적인 설계 (0) | 2024.07.23 |