2026. 6. 4. 19:32ㆍ프로젝트/Trip-Linker 팀 프로젝트
계정 보안 이력 관리 Entity·Repository 구현
📋 한줄 요약
로그인 실패 횟수 누적·계정 잠금·비밀번호 변경 이력을 DB에 기록하기 위한 Entity와 Repository를 구현했다.
🎯 왜 만들었나
계정 보안 기능(5회 실패 시 5분 잠금, 90일마다 비밀번호 변경 권장)을 구현하려면
상태값을 User 엔티티에 저장하고, 보안 이벤트 발생 시 이력을 별도 테이블에 남겨야 했다.
📁 작업 순서 (파일별)
1. SecurityEventType.java
📦 domain/user/entity
추가한 코드
public enum SecurityEventType {
PW_CHANGE, // 비밀번호 변경 완료
PW_CHANGE_NOTIFIED // 90일 경과 변경 권장 모달 노출
}
왜 만들었나
DB의 event_type 컬럼에 문자열을 그대로 넣으면 오타·조건 분기가 위험하다.
Enum으로 타입을 고정해서 컴파일 타임에 오류를 잡을 수 있게 했다.
2. User.java
📦 domain/user/entity
추가한 컬럼
@Column(name = "login_fail_count", nullable = false)
private int loginFailCount = 0; // 연속 로그인 실패 횟수
@Column(name = "locked_until")
private LocalDateTime lockedUntil; // 잠금 해제 시각
@Column(name = "last_pw_changed_at")
private LocalDateTime lastPwChangedAt; // 마지막 비밀번호 변경 시각
@Column(name = "pw_change_noti_at")
private LocalDateTime pwChangeNotiAt; // 90일 변경 권장 모달 노출 시각
@Column(name = "deleted_at")
private LocalDateTime deletedAt; // 탈퇴 처리 시각
// created_at / updated_at — @PrePersist / @PreUpdate 로 자동 세팅
추가한 메서드
// 잠금 상태 확인
public boolean isLocked() {
return lockedUntil != null && LocalDateTime.now().isBefore(lockedUntil);
}
// 로그인 실패 시 호출 — 5회 도달 시 5분 잠금
public void increaseLoginFailCount() {
this.loginFailCount++;
if (this.loginFailCount >= 5) {
this.lockedUntil = LocalDateTime.now().plusMinutes(5);
}
}
// 로그인 성공 시 호출 — 실패 횟수 및 잠금 초기화
public void resetLoginFail() {
this.loginFailCount = 0;
this.lockedUntil = null;
}
// 비밀번호 변경 시 호출
public void updatePassword(String newPasswordHash) {
this.passwordHash = newPasswordHash;
this.lastPwChangedAt = LocalDateTime.now();
}
// 90일 권장 모달 노출 시각 기록
public void recordPwChangeNotified() {
this.pwChangeNotiAt = LocalDateTime.now();
}
왜 이 구조로 만들었나
보안 상태 변경 로직(잠금, 초기화, 비밀번호 변경)을 Service가 아닌 Entity 메서드 안에 넣었다.
Service에서 필드를 직접 건드리지 않고 메서드를 호출하게 해서,
잠금 조건(5회)이 바뀌어도 User.java 한 곳만 수정하면 된다.
@PrePersist / @PreUpdate 를 쓴 이유
@PrePersist
protected void onCreate() {
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
this.updatedAt = LocalDateTime.now();
}
JPA가 INSERT/UPDATE 직전에 자동으로 호출해준다.
LocalDateTime.now()를 코드 곳곳에서 직접 세팅하지 않아도 되고,
시각 누락 실수를 방지할 수 있다.
3. UserSecurityHistory.java
📦 domain/user/entity
추가한 코드
@Entity
@Table(name = "USER_SECURITY_HISTORY")
public class UserSecurityHistory {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
@Enumerated(EnumType.STRING)
@Column(name = "event_type", nullable = false, length = 30)
private SecurityEventType eventType;
@Column(name = "description", length = 200)
private String description;
// 정적 팩토리 메서드
public static UserSecurityHistory of(
User user, SecurityEventType eventType, String description) {
UserSecurityHistory h = new UserSecurityHistory();
h.user = user;
h.eventType = eventType;
h.description = description;
return h;
}
}
왜 이 구조로 만들었나
@NoArgsConstructor(access = AccessLevel.PROTECTED) — 기본 생성자를 외부에서 못 쓰게 막았다.
JPA는 내부적으로 기본 생성자가 필요하지만, new UserSecurityHistory() 로 직접 만드는 것을 막아
반드시 of() 정적 팩토리 메서드를 통해서만 생성하도록 강제했다.
@ManyToOne(fetch = FetchType.LAZY) — 이력을 조회할 때 User 전체를 즉시 JOIN하면 불필요한 쿼리가 나간다.
LAZY로 설정해서 실제로 User 정보가 필요할 때만 추가 쿼리가 나가게 했다.
@Enumerated(EnumType.STRING) — DB에 숫자(0, 1) 대신 문자열(PW_CHANGE)로 저장한다.
ENUMTYPE.ORDINAL은 Enum 순서가 바뀌면 기존 데이터가 깨지는 위험이 있어서
STRING 방식을 선택했다.
4. UserSecurityHistoryRepository.java
📦 domain/user/repository
추가한 코드
public interface UserSecurityHistoryRepository
extends JpaRepository<UserSecurityHistory, Long> {
List<UserSecurityHistory> findByUserIdOrderByCreatedAtDesc(Long userId);
}
왜 이렇게 만들었나
Spring Data JPA의 메서드 이름 규칙을 사용했다.
findByUserId — user_id 조건 WHERE 절 자동 생성
OrderByCreatedAtDesc — 최신순 정렬 자동 생성
별도 JPQL 없이 메서드 이름만으로 쿼리가 완성된다.
💡 배운 점
Entity에 비즈니스 메서드를 넣는 방식(도메인 모델 패턴)을 처음 적용해봤다.
Service에서 필드를 직접 수정하는 대신 Entity 메서드를 호출하면
조건 변경 시 수정 범위가 줄어들고 테스트하기도 쉬워진다.
@Enumerated(EnumType.STRING) 은 거의 항상 STRING을 써야 한다는 것을 배웠다.
ORDINAL은 Enum 중간에 값이 추가되면 기존 데이터가 전부 깨진다.
'프로젝트 > Trip-Linker 팀 프로젝트' 카테고리의 다른 글
| Trip-Linker 개발일지 6 - 플래너 API 연동 및 UI 개선 — goPlanStep 검증, 날씨 팝업 (0) | 2026.06.04 |
|---|---|
| Trip-Linker 개발일지 5 - User.java — updateNickname·withdraw 메서드 추가 (0) | 2026.06.04 |
| Trip-Linker 개발일지 4 - auth 도메인 전체 구현 — 로그인·토큰·비밀번호 재설정 (0) | 2026.06.04 |
| Trip-Linker 개발일지 3 - 프론트 리팩토링 — MOCK 위치, go() 렌더러, CSS 버그 (0) | 2026.06.04 |