🐞 JPA 엔티티에서 equals()와 hashCode() 구현 시 반드시 주의할 점 - 직접 필드 접근이 왜 버그를 유발하는가?


 

✅ 상황 설명: 분명히 필드 값은 같았는데, equals() 비교가 false?

JPA를 사용할 때 다음과 같은 상황을 경험해본 적이 있을 수 있습니다:

Member m1 = memberRepository.findById(1L).get();
Member m2 = new Member(1L);
System.out.println(m1.equals(m2)); // false?!

두 객체는 같은 ID를 가지고 있지만 equals()는 false를 반환합니다.
왜 이런 일이 벌어질까요?


 

🔍 원인: 프록시(proxy)와 직접 필드 접근의 충돌

JPA에서 엔티티는 일반적인 자바 객체로 생성되는 것이 아니라,
프록시 객체로 래핑되어 반환되는 경우가 많습니다.

예를 들어, @ManyToOne 등 지연 로딩(LAZY)이 걸린 엔티티는 실제 객체가 아니라 하이버네이트가 만든 프록시 객체입니다.

🔎 그런데?

우리가 equals()나 hashCode() 안에서 직접 필드를 비교하면,
이 프록시 객체는 실제 필드에 접근하지 못하거나,
클래스 타입이 달라서 비교가 틀어지는 문제가 발생합니다.


 

❌ 잘못된 구현 예시

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false; // ⛔ 위험
    Member member = (Member) o;
    return id != null && id.equals(member.id); // ⛔ 직접 필드 접근

⚠️ 이 방식의 문제

  • getClass()를 사용하면 프록시 객체는 통과하지 못함 → equals 실패
  • member.id를 직접 접근하면, 아직 초기화되지 않았거나 프록시인 경우 올바르게 비교되지 않음

✅ 올바른 접근 방식: getter 사용

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof Member)) return false;
    Member other = (Member) o;
    return this.getId() != null && this.getId().equals(other.getId()); // ✅ getter 사용
}

📌 이 방식의 장점

  • 프록시 객체도 instanceof를 통과할 수 있음
  • getter를 통해 필드에 접근하면, 프록시가 내부적으로 위임하여 실제 값에 접근 가능
  • 하이버네이트 프록시 내부 동작에 맞게 작동함 → 안정적

📦 실무 팁: equals/hashCode는 오직 식별자(ID)만 기준으로

JPA에서는 equals()와 hashCode()는 반드시 식별자(ID)만을 기준으로 구현해야 합니다.

@Override
public int hashCode() {
    return Objects.hash(getId());
}

 

왜냐하면:

  • 아직 persist 되지 않은 엔티티는 ID가 없을 수 있고
  • 다른 필드를 비교 기준으로 삼으면 상태 변화에 따라 해시값이 바뀔 수 있어 HashSet 등에서 문제 발생

📌 요약 정리

항목 설명
문제 원인 프록시 객체와 직접 필드 접근의 충돌
잘못된 구현 getClass(), 직접 필드 접근
올바른 구현 instanceof + getter 사용
비교 대상 항상 식별자(ID) 하나만 비교
해시코드 ID 기반으로만 계산

 

✅ 마무리

Spring Data JPA에서 엔티티는 단순한 자바 객체처럼 보이지만,
실제로는 하이버네이트가 감싸는 프록시 객체일 수 있습니다.
따라서 equals()와 hashCode()를 구현할 때 직접 필드에 접근하는 방식은 매우 위험합니다.

 

📌 반드시 getter를 통해 접근하고,
비교 기준은 ID 하나만 사용하는 것이 안전한 JPA 실무 전략입니다.

'백엔드 > JPA' 카테고리의 다른 글

DTYPE 컬럼  (0) 2025.04.10

🔄 Spring MVC의 Redirect vs Forward 완벽 정리
- 요청 흐름을 제어하는 두 가지 방법

 

✅ Redirect와 Forward란?

웹 애플리케이션에서는 컨트롤러에서 다른 URL로 이동시키고 싶을 때가 많습니다.
Spring MVC에서는 이를 위해 Redirect(리다이렉트)Forward(포워드) 두 가지 방법을 제공합니다.


 

🔁 1. Forward (포워드)

📌 개념

서버 내부에서 한 컨트롤러 → 다른 컨트롤러 or 뷰로 요청을 전달
브라우저 주소창은 변하지 않음

🔧 예시 코드

@GetMapping("/step1")
public String forwardExample() {
    return "forward:/step2";
}
  • /step1 요청이 들어오면 서버 내부적으로 /step2로 이동
  • 클라이언트는 여전히 /step1을 보고 있음
  • 같은 request 객체 공유

✅ 특징

항목 설명
요청 방식 서버 내부에서 요청 전달
주소창 변화 ❌ 없음
request 유지 ✅ 유지됨
속도 빠름 (서버 내부 이동이므로)
주 사용처 내부 흐름 제어, request 데이터를 그대로 넘길 때

 

🔀 2. Redirect (리다이렉트)

📌 개념

클라이언트에게 응답을 보내어, 브라우저가 새로운 요청을 하도록 유도
주소창이 새 주소로 변경됨

🔧 예시 코드

@PostMapping("/form")
public String redirectExample() {
    return "redirect:/success";
}
  • /form 요청을 처리한 후, 브라우저에게 /success로 다시 요청하라고 지시
  • 브라우저 주소창이 /success로 바뀜
  • 완전히 새로운 request

✅ 특징

항목 설명
요청 방식 브라우저가 새 요청 수행
주소창 변화 ✅ 바뀜
request 유지 ❌ 사라짐 (new request)
속도 느림 (두 번 요청)
주 사용처 PRG(Post-Redirect-Get), URL 변경 필요 시, 중복 요청 방지 등

 

🔄 Forward vs Redirect 차이 한눈에 보기

항목 Forward Redirect
주소창(URL) 유지됨 (/step1) 변경됨 (/success)
요청 횟수 1회 (서버 내부) 2회 (클라이언트가 다시 요청)
request 데이터 유지됨 (공유 가능) 유지되지 않음 (필요 시 Flash 사용)
주요 사용 시점 내부 이동, 서버 흐름 제어 폼 제출 후 이동, 새 페이지 유도
속도 상대적으로 빠름 상대적으로 느림

 

🧪 실전 예제: 로그인 후 리다이렉트

@PostMapping("/login")
public String login(@RequestParam String id, Model model) {
    boolean success = authService.login(id);

    if (success) {
        return "redirect:/home";
    } else {
        model.addAttribute("msg", "로그인 실패");
        return "loginForm"; // JSP 그대로 렌더링
    }
}
  • 로그인 성공 → Redirect → 새 페이지 이동 (/home)
  • 로그인 실패 → 기존 요청 유지 → 같은 페이지 렌더링

💡 Flash Attributes와 함께 쓰기 (Redirect 시 request 유지 대안)

@PostMapping("/submit")
public String submit(RedirectAttributes redirectAttributes) {
    redirectAttributes.addFlashAttribute("msg", "저장 완료!");
    return "redirect:/result";
}

@GetMapping("/result")
public String resultPage(@ModelAttribute("msg") String msg) {
    // FlashAttribute는 1회성 request로 전달됨
    return "result";
}

✔ RedirectAttributes는 Redirect 시에도 데이터를 1회성으로 안전하게 전달할 수 있게 해주는 도구입니다.


🔚 마무리 요약

상황 Forward 사용 Redirect 사용
서버 내부 이동
브라우저 URL 변경
request 데이터 유지 ❌ (Flash로 대체)
중복 요청 방지 ✅ (PRG 패턴)
성능 측면 빠름 느림 (2번 요청)

Forward는 내부 흐름 제어,
Redirect는 외부 이동 또는 사용자 UX 제어에 적합합니다.

 

상황에 따라 두 가지 방식을 적절히 활용하면,
리소스를 아끼고 사용자 경험을 향상시키는 웹 애플리케이션을 만들 수 있습니다. 😊

 

 

🔍 Lombok @ToString(callSuper = true)란?
- 상속 구조에서 부모 클래스의 필드까지 출력하려면?


✅ @ToString 어노테이션이란?

Lombok에서 제공하는 @ToString은 클래스의 toString() 메서드를 자동으로 생성해주는 어노테이션입니다.

@Getter
@ToString
public class User {
    private String name;
    private int age;
}

위 코드에서 User 객체의 toString()을 호출하면 다음과 같은 문자열을 자동으로 출력합니다:

User(name=철수, age=30)

 


❓ 그런데 상속받은 클래스라면?

public class BaseEntity {
    private LocalDateTime createdAt;
}

@Getter
@ToString
public class Member extends BaseEntity {
    private String name;
}

이 경우 Member의 toString() 결과는 다음과 같습니다:

 
Member(name=철수)

⚠️ 부모 클래스인 BaseEntity의 createdAt 필드는 출력되지 않습니다!


🧩 해결 방법: @ToString(callSuper = true)

@Getter
@ToString(callSuper = true)
public class Member extends BaseEntity {
    private String name;
}

이렇게 하면 toString() 결과에 부모 클래스의 필드까지 포함됩니다:

Member(super=BaseEntity(createdAt=2024-05-18T22:01:23.456), name=철수)

 

📌 언제 사용하면 좋을까?

상황 사용 여부
DTO나 로그 출력을 위해 전체 필드를 보고 싶을 때 ✅ 사용 권장
부모 클래스에도 중요한 값이 있고, 이를 확인해야 할 때 ✅ 사용
부모 클래스에 민감하거나 불필요한 값이 있을 때 ❌ 사용 주의 (민감 정보 노출 위험)
 

⚠️ 주의사항

  1. 상속 구조가 복잡할수록 출력이 길어질 수 있음
    → 지나치게 많은 정보가 노출될 수 있음
  2. 보안 이슈 주의
    → 부모 클래스에 비밀번호, 인증토큰 등 민감 정보가 포함되면 toString()으로 노출될 수 있음
  3. 무한 순환 참조에 주의
    → @ToString.Exclude를 활용해 순환 필드는 제외할 수 있음
@ToString.Exclude
private Member member; // 예: 양방향 참조
 

 

✅ 마무리 정리

항목 설명
어노테이션 @ToString(callSuper = true)
목적 부모 클래스의 필드도 toString()에 포함
주 사용처 상속 구조에서 로그 출력, 디버깅 시 전체 정보 확인
주의사항 민감 정보, 순환 참조, 출력 과다 가능성 고려 필요

@ToString(callSuper = true)는 단순히 로그 예쁘게 보이기 위한 기능이 아니라,
상속 구조에서 정보 누락 없이 객체 상태를 파악할 수 있게 도와주는 도구입니다.
단, 모든 정보를 노출하는 만큼 신중하게 사용하는 것이 좋습니다. 😊

 

 

🧩 Java default method란?

- 인터페이스에 구현을 허용한 이유와 쓰임 정리


✅ default method란?

자바 8부터, 인터페이스 안에 메서드의 기본 구현(body)을 작성할 수 있게 되었고,
이때 사용하는 키워드가 바로 default입니다.

public interface MyInterface {
    default void greet() {
        System.out.println("Hello from default method!");
    }
}

✔ 즉, 인터페이스지만 메서드 구현이 가능해진 겁니다.


🔍 왜 생겼을까? (등장 배경)

자바 8에서는 Stream, Lambda, Functional Interface 등 다양한 API가 추가되었고,
이를 기존 인터페이스에 기능을 추가해야 하는 상황이 생겼습니다.

하지만 기존 방식대로라면…

  • 인터페이스에 메서드를 추가하면 모든 구현체가 컴파일 에러가 납니다!
    → 기존 라이브러리와 하위 호환 깨짐 😥

그래서!

기본 구현이 있는 메서드를 인터페이스에 추가할 수 있도록
default method가 도입되었습니다. (하위 호환 확보!)

 


🛠 사용 예제

public interface Vehicle {
    void start();

    default void honk() {
        System.out.println("빵빵!");
    }
}

public class Car implements Vehicle {
    public void start() {
        System.out.println("자동차 시동 켜짐");
    }
}

public class Main {
    public static void main(String[] args) {
        Vehicle car = new Car();
        car.start(); // 자동차 시동 켜짐
        car.honk();  // 빵빵!
    }
}

🔎 포인트

  • Car 클래스는 honk()를 구현하지 않았지만, Vehicle에서 기본 구현이 제공되므로 바로 사용 가능
  • 기존 인터페이스에 새로운 메서드를 추가해도 구현체는 깨지지 않음

🧠 default method는 override도 가능

public class Truck implements Vehicle {
    public void start() {
        System.out.println("트럭 시동 켜짐");
    }

    @Override
    public void honk() {
        System.out.println("트럭 빵빵!");
    }
}

→ Truck에서는 honk() 메서드를 재정의해서 자신만의 동작을 가질 수 있어요.


⚠️ 주의할 점

항목 설명
다중 상속 충돌 두 인터페이스에 동일한 default 메서드가 있으면 명시적으로 오버라이드 해야 함
상태 유지 불가 인터페이스는 상태(필드)를 가질 수 없기 때문에 default method 내에서는 필드 사용 불가
남용 금지 너무 많은 로직을 default method에 넣으면 인터페이스가 무거워지고, 객체지향 설계 원칙을 해칠 수 있음
 

❗ 다중 상속 충돌 예시

interface A {
    default void hello() { System.out.println("Hello from A"); }
}

interface B {
    default void hello() { System.out.println("Hello from B"); }
}

class C implements A, B {
    @Override
    public void hello() {
        A.super.hello(); // 또는 B.super.hello()
    }
}

 


📌 default method vs abstract method

구분 abstract method default method
구현 여부 ❌ 없음 ✅ 있음
오버라이드 필요 여부 ✅ 반드시 구현해야 함 ❌ 선택적으로 구현 가능
목적 공통 규약 선언 하위 호환 유지 및 공통 로직 제공

 


✅ 실무에서의 활용 팁

  • 자주 반복되는 공통 동작을 미리 구현해줄 때 유용
  • 하위 호환을 보장하며 API를 확장할 때 적합
  • 인터페이스지만 전략 패턴처럼 일부 기본 동작을 제공하고, 필요한 부분만 구현하도록 유도할 수 있음

🔚 마무리 정리

항목 요약
정의 인터페이스에서 메서드 구현을 허용하는 기능 (default 키워드)
목적 하위 호환성과 공통 구현 제공
사용 시 주의 다중 상속 충돌, 상태 불가, 남용 금지
실전 팁 전략 패턴, 유틸성 메서드, 기존 API 확장 등에 적합

 

 

default method는 단순한 문법이 아니라,
자바 인터페이스의 역할을 확장하고 객체지향을 유연하게 만드는 기능입니다.
적절히 활용하면 코드 중복을 줄이고 유지보수성을 높일 수 있습니다.

 

🧪 실전 예제로 배우는 Git Flow 전략
- 기능 개발부터 릴리스, 버그 수정까지 한눈에 흐름 정리


📘 시나리오

지금부터 MyShop이라는 쇼핑몰 프로젝트에서 로그인 기능 개발 → 버전 릴리스 → 긴급 버그 수정이라는 흐름을 Git Flow 전략에 따라 진행해보겠습니다.


1️⃣ 프로젝트 초기화

# 기본 브랜치(main)는 이미 존재한다고 가정
git checkout -b develop
git push -u origin develop
  • main은 항상 배포 가능한 안정 코드
  • develop은 다음 릴리스를 위한 통합 개발 브랜치

2️⃣ 기능 개발 (로그인 기능)

# develop에서 새로운 기능 브랜치 생성
git checkout develop
git checkout -b feature/login

# 로그인 기능 개발 작업
# 파일 수정, 커밋...
git add .
git commit -m "feat: 로그인 기능 구현"

# 기능 완료 후 develop에 병합
git checkout develop
git merge feature/login
git branch -d feature/login

🔄 정리

  • feature/login 브랜치에서 기능을 개발하고
  • 완료되면 develop으로 병합 후 삭제

3️⃣ 릴리스 준비 (v1.0.0)

# release 브랜치 생성
git checkout develop
git checkout -b release/1.0.0

# 테스트 중 발견된 마이너 이슈 수정
git commit -am "fix: 로그인 버튼 색상 조정"

# 릴리스 완료 → main에 병합
git checkout main
git merge release/1.0.0
git tag -a v1.0.0 -m "🎉 Release v1.0.0"

# develop에도 병합 (테스트 수정사항 반영)
git checkout develop
git merge release/1.0.0

# release 브랜치 삭제
git branch -d release/1.0.0

✅ main은 최신 배포 버전
✅ develop은 다음 기능 개발 준비 완료


4️⃣ 긴급 버그 수정 (v1.0.1)

배포 후 로그인 시 예외 발생 보고됨! 🔥

# main에서 hotfix 브랜치 생성
git checkout main
git checkout -b hotfix/login-crash

# 문제 수정
git commit -am "fix: 로그인 시 NullPointerException 발생 문제 해결"

# 수정 완료 → main에 병합
git checkout main
git merge hotfix/login-crash
git tag -a v1.0.1 -m "🔧 Hotfix: 로그인 버그 수정"

# develop에도 병합 (다음 개발에 반영)
git checkout develop
git merge hotfix/login-crash

# hotfix 브랜치 삭제
git branch -d hotfix/login-crash

🔄 전체 흐름 요약

main
 └─ release/1.0.0 → 🔀 merge → main + develop (릴리스)
 └─ hotfix/login-crash → 🔀 merge → main + develop (버그 수정)

develop
 └─ feature/login → 🔀 merge → develop (기능 개발)

📌 브랜치별 용도 다시 보기

브랜치 용도
main 운영 중인 최종 코드 (배포 버전)
develop 다음 릴리스를 위한 통합 개발 브랜치
feature/* 기능 단위 개발 (개발자 개인 작업 공간)
release/* 배포 전 테스트/수정/버전 태깅
hotfix/* 운영 중 긴급 수정 및 패치

🧠 팁: Git Flow는 언제 쓰면 좋을까?

  • ✅ 협업 인원이 3명 이상이고
  • ✅ 기능/릴리스/버그 수정이 병렬적으로 발생하며
  • ✅ 릴리스 주기가 명확히 존재할 때

이런 상황이라면 Git Flow는 버전 관리의 복잡성을 줄이고, 팀 간 충돌을 줄여줍니다.


✅ 마무리

Git Flow는 단순히 브랜치를 나누는 것이 아니라,
기능 개발, 테스트, 릴리스, 유지보수 작업의 전체 흐름을 시각적으로 설계하게 해줍니다.

실제 프로젝트에서도 이번 예시처럼

  • 기능 단위로 feature/* 브랜치를 만들고
  • 안정성 테스트를 release/*에서 진행하며
  • 문제가 생기면 hotfix/*로 바로 대응하는 방식이
    협업의 효율성과 서비스 안정성을 크게 높여줍니다.

 

 

'Git > GitHub' 카테고리의 다른 글

Git Branch 전략 - Git Flow  (0) 2025.05.03
GitHub - Image 올리기 (README, Issue, PR)  (0) 2021.08.22

🚦Git Flow 전략 완벽 정리
- 협업을 위한 체계적인 브랜치 관리 전략


✅ Git Flow란?

Git Flow는 Vincent Driessen이 제안한 Git 브랜치 관리 전략으로,
여러 명이 협업할 때 개발, 배포, 수정 작업을 안정적으로 나누어 진행할 수 있도록 체계를 잡아주는 방법입니다.

기본적으로 역할이 명확한 브랜치를 정의하고,
기능 개발 → 테스트 → 배포로 이어지는 일련의 과정을 브랜치 흐름으로 시각화합니다.


🏷️ Git Flow의 5가지 주요 브랜치

브랜치 역할
main 최종 배포된 제품 버전이 존재 (항상 안정적인 코드)
develop 다음 버전을 위한 통합 개발 브랜치, 기능들이 이곳에 모임
feature/* 새로운 기능 개발용 브랜치 (feature/login 등)
release/* 배포 전 단계에서 테스트/버그 수정 진행 (release/1.0.0)
hotfix/* 배포된 main 브랜치에 치명적 버그가 발생했을 때 긴급 수정용 (hotfix/urgent-bug)

🧭 Git Flow 작업 흐름

1. main → 가장 안정적인 배포 코드
2. develop → 기능들을 병합해가는 통합 브랜치

[기능 개발 단계]
- develop → feature/* → develop

[배포 준비 단계]
- develop → release/* → main + develop

[긴급 버그 수정]
- main → hotfix/* → main + develop

🧪 예시 흐름 보기

✅ 기능 개발

git checkout develop
git checkout -b feature/login
# 작업 후
git commit -m "Add login feature"
git checkout develop
git merge feature/login
git branch -d feature/login

✅ 릴리즈 준비

git checkout develop
git checkout -b release/1.0.0
# 테스트 및 버그 수정
git commit -m "Fix minor issues"
git checkout main
git merge release/1.0.0
git tag -a v1.0.0 -m "Release 1.0.0"
git checkout develop
git merge release/1.0.0
git branch -d release/1.0.0

✅ 긴급 수정

git checkout main
git checkout -b hotfix/urgent-fix
# 버그 수정
git commit -m "Fix production bug"
git checkout main
git merge hotfix/urgent-fix
git tag -a v1.0.1 -m "Hotfix release"
git checkout develop
git merge hotfix/urgent-fix
git branch -d hotfix/urgent-fix

📦 Git Flow 전략의 장점

항목 설명
✅ 역할 구분 명확 브랜치마다 목적이 분리되어 협업에 유리
✅ 배포 시점 관리 쉬움 release, hotfix로 안정된 배포 프로세스 가능
✅ 충돌 관리 용이 기능 별 feature 브랜치로 분산 작업 가능
✅ 안정성 확보 main은 항상 배포 가능한 상태로 유지됨

⚠️ 단점 및 실무에서의 보완점

항목 설명
❗ 브랜치가 많아짐 브랜치 관리가 복잡해지고 무거워질 수 있음
❗ 빠른 배포와는 거리 있음 CI/CD 파이프라인과 속도가 안 맞을 수 있음
❗ 릴리즈 주기가 짧으면 비효율 release 브랜치가 너무 자주 생기면 오히려 불편함

💡 실무 팁

  • 1~3인 규모의 사이드 프로젝트에는 단순한 main + feature/*만 써도 충분함
  • 단순화된 GitHub Flow 또는 Trunk Based Development와 혼용하기도 함
  • 릴리즈 자동화(CI/CD)와 함께 쓰려면 release 브랜치를 과감히 생략하기도 함

🔚 마무리

Git Flow는 역할이 명확한 브랜치 전략으로,
개발/테스트/배포 과정을 체계적으로 관리할 수 있게 도와주는 훌륭한 도구입니다.

협업이 많아지고 릴리즈 주기가 명확한 팀이라면,
Git Flow는 팀 생산성과 품질을 동시에 높여줄 수 있는 브랜치 전략입니다.

 

 

 

'Git > GitHub' 카테고리의 다른 글

Git Flow 전략 - 실전 예시  (0) 2025.05.03
GitHub - Image 올리기 (README, Issue, PR)  (0) 2021.08.22

+ Recent posts