2/20(금) 엔티티 연관, Git 특강

dev_joo·2026년 2월 20일

코드 카타

1. 나누어 떨어지는 숫자

조건맞는 요소 추가 + 가변배열->정적배열

import java.util.*;

class Solution {
    public int[] solution(int[] arr, int divisor) {
        List<Integer> list = new ArrayList<>();
        for(int a : arr){
            if(a%divisor == 0) list.add(a);
        }
        if(list.size() == 0) return new int[] {-1};
        int[] answer = new int[list.size()];
        for (int i = 0; i < list.size(); i++) {
            answer[i] = list.get(i);
        }
        Arrays.sort(answer);
        return answer;
    }
}

List 방식 대신 stream을 사용할 수 있다.

Arrays.stream(arr)
      .filter(n -> n % divisor == 0)
      .sorted()
      .toArray();

2. 음양 더하기

class Solution {
    public int solution(int[] absolutes, boolean[] signs) {
        int answer = 0;
        for(int i = 0; i< signs.length; i++){
            answer += signs[i] ? absolutes[i] : -absolutes[i];
        }
        return answer;
    }
}

배열[i] 앞에 부호를 붙여도 되나 싶어 해봤는데 이게 되네? 했던 코드😅
확인 결과 양수에 +를 붙여도 동작한다.

자바에서 단항 연산자 + / - 가 가능한 타입:

byte
short
int
long
float
double
char (정수로 승격됨)

엔티티 연관관계

1:1

제대로 이해했는지 확인하기 위해 강의에서 나오는 외래키의 주인을 Food에서 User로 바꿔 작성했다.

@OneToOne

@OneToOne 애너테이션은 1:1 관계를 맺어주는 역할을 한다.

@JoinColum

@JoinColum 애너테이션은 해당 테이블에 저장될 외래키 이름을 지정한다.

단방향 관계

외래 키의 주인

고객 Entity가 외래 키의 주인인 경우:
👩‍💼: 내가 시킨 음식은...🍔 이다.

@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

	// Food를 필드로 가진다.
    @OneToOne
    @JoinColumn(name = "food_id")
    private Food food;
}

🍔: ????

@Entity
@Table(name = "foods")
public class Food {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private double price;
}

양방향 관계

양방향 관계에서는 각 엔티티가 서로를 필드로 가지고있다.
👩‍💼: 내가 시킨 음식은...🍔 이다.

@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @OneToOne
    @JoinColumn(name = "food_id") // 외래키 이름 지정
    private Food food;
}

🍔: 나는... 👩‍💼에게 시켜졌다.

@Entity
@Table(name = "foods")
public class Food {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private double price;

    @OneToOne(mappedBy = "food") // 상대 Entity의 "필드"명, users테이블은 외래키를 가지게 된다.
    private User user;
}

@OneToOne(mappedBy 옵션)

이 때, DB에서 외래 키는 연관관계를 가진 두 테이블 중 한쪽에만 저장된다. 그래서 JPA에서는 외래키의 주인이 어떤 엔티티인지 정보를 저장해야한다.
이 때, @OneToOne애너테이션의 mappedBy 옵션으로 해당 필드를 어떤 Entity가 가지고 있는지 상대 Entity의 필드명을 적어준다.

@OneToOne(mappedBy = "food")
private User user;

❓ 이미 외래키를 가진 엔티티에 작성이되어있는데 왜 반대편에서 mappedBy로 또 명시해야 하지? JPA가 자동으로 추론해서 연결해주면 안 되나?”

@OneToOne
    @JoinColumn(name = "food_id") // 외래키 이름 지정
    private Food food;
  • FK 컬럼이 어디 있는지
  • 어떤 엔티티를 참조하는지
  • 어떤 필드가 DB를 수정하는지

JPA는 DB의 FK 정보를 기반으로 동작하지만, 매핑은 객체 필드 단위로 정의된다.
JPA는 각 필드를 독립 관계로 보기 때문에, 두 필드가 같은 관계로 이어져있다 추론하지 않는다. mappedBy는 기존 관계를 재사용한다고 명시해 중복 매핑과 의미 혼동을 막기 위한 장치다.

외래키 조작은 DB에서 외래키를 가지고있을 엔티티만 가능하다

@Test
@Rollback(false)
@DisplayName("1대1 양방향 테스트 : 외래 키 저장 실패")
/**
 * 외래키 주인인 User 엔티티만 외래키 필드를 직접 조작할 수 있다
 */
void testUserAsOwner() {
    Food food = new Food();
    food.setName("고구마 피자");
    food.setPrice(30000);

    // 외래키 주인인 User에서 Food를 설정
    User user = new User();
    user.setName("Robbie");
    user.setFood(food); // O

    // 외래키 주인이 아닌 Food에서 User를 설정하면 DB food_id 값이 null로 비어있다.
    // food.setUser(user); // BUG: 단순 설정만으로는 FK 저장 불가
	
    // 외래 키(연관 관계) 설정 Food.user.setFood(this); 를 추가한 food.addUser(user); 등의 메서드를 만들어 해결한다.
    food.addUser(user);
    
    userRepository.save(user);
    foodRepository.save(food);

}

양방향 조회는 양쪽 방향에서 모두 가능하다

@Test
@DisplayName("1대1 조회 : User 기준 food 정보 조회")
void test6() {
    User user = userRepository.findById(1L).orElseThrow(NullPointerException::new);
    // 고객 정보 조회
    System.out.println("user.getName() = " + user.getName());

    // 해당 고객이 주문한 음식 정보 조회
    Food food = user.getFood();
    System.out.println("food.getName() = " + food.getName());
    System.out.println("food.getPrice() = " + food.getPrice());
}

@Test
@DisplayName("1대1 조회 : Food 기준 user 정보 조회")
void test5() {
    Food food = foodRepository.findById(1L).orElseThrow(NullPointerException::new);
    // 음식 정보 조회
    System.out.println("food.getName() = " + food.getName());

    // 음식을 주문한 고객 정보 조회
    System.out.println("food.getUser().getName() = " + food.getUser().getName());
}

중간 테이블이 생성되는 경우

JPA에 외래키를 어디에 저장할 지 지정되지 않았을 때 중간 테이블이 생성된다.

1. 양방향 관계에서 mappedBy옵션을 지정하지 않았을 때
2. @JoinColumn()의 생략해 외래키 위치가 지정되지 았았을 때

단, 1:1 양방향 관계에서는 외래 키의 주인 Entity에서는 mappedBy로 상태 엔티티의 필드명을 표시해 @JoinColumn() 애너테이션을 사용하지 않아도 default 옵션으로 외래키를 생성이 적용되기 때문에 생략이 가능하다.

1:N , N:1

@OneToMany

@OneToMany 애너테이션은 1:N 관계에서 1쪽 엔티티에서 사용하며, 여러 개의 자식(N)을 관리한다.

@ManyToOne

@ManyToOne 애너테이션은 N:1 관계에서 N쪽 엔티티에서 사용하며, 하나의 부모(1)를 참조한다.

OneToMany 단방향 관계?

OneToMany 단방향에서는 외래키가 N쪽 테이블에 있음에도
JPA가 1쪽에서 외래키를 관리하려 하므로 INSERT 이후 UPDATE가 추가로 발생하는 성능상 비효율이 생긴다.

@Entity
@Table(name = "users")
public class User {
   @Id
   @GeneratedValue(strategy = GenerationType.IDENTITY)
   private Long id;
   private String name;
   
   // 현실적 구현에서는 @ManyToOne이 있는 N쪽에서 외래 키를 관리하는 것이 안전
   // INSERT/UPDATE 시 성능 문제가 발생할 수 있다.
   // 실제로는 Food 테이블에 외래 키가 생기지만, JPA는 user_id 컬럼을 직접 관리
   @OneToMany
   @JoinColumn(name = "user_id") // foods 테이블에 user_id 컬럼
   private List<Food> foodList = new ArrayList<>();
  
}
@Entity
@Table(name = "food")
public class Food {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private double price;
}

OneToMany 단방향에서 기존 User 수정 시 JPA 동작
OneToMany 단방향에서 @JoinColumn을 사용하면
연관관계의 주인이 User(1쪽) 가 된다.
하지만 실제 외래키는 foods 테이블(N쪽) 에 존재한다.
따라서 flush() 시점에 JPA는 다음과 같이 동작한다:

  1. User 엔티티의 변경 사항을 감지하여 users 테이블을 UPDATE 한다.
  2. @OneToMany 컬렉션의 변경 여부를 스냅샷 기반으로 비교한다.
  3. foodList에 포함된 Food 엔티티들에 대해,
    외래키(user_id) 값을 반영하기 위해 컬렉션에 포함된 Food 개수만큼 UPDATE를 실행할 수 있다.
UPDATE foods SET user_id = ? WHERE id = ?

💡 그래서 @ManyToOne이 있는 N쪽에서 외래 키를 관리하는 것이 좋다.
N쪽(Food)에 @ManyToOne으로 외래키를 직접 설정하면,
Food INSERT 시점에 user_id를 함께 저장할 수 있으므로

INSERT INTO foods (name, price, user_id) VALUES (?, ?, ?)

추가 UPDATE가 발생하지 않는다.

양방향 관계?

1:1 관계에서는 mappedBy옵션을 통해 양방향 관계를 설정해줄 수 있었다.

그러나 1대 N관계에서는 일반적으로 양방향 관계가 존재하지 않는다.
@ManyToOne 애너테이션은 mappedBy 속성을 제공하지 않는다.

양방향을 구현할 수는 있지만 외래키를 읽기만 하고 절대 수정하지 않는 설정이다.

N쪽이 FK를 관리하는 단방향 관계만으로도 충분하다.

@ManyToOne과 @JoinColumn 의 insertable, updatable 옵션으로 연결

N 관계의 Food Entity에서 @JoinColumninsertableupdatable옵션을 false로 설정하여 양쪽으로 JOIN 설정을 하면 양방향 처럼 설정할 수는 있다.

@Entity
@Table(name = "foods")
public class Food {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private double price;
    
    @ManyToOne
    @JoinColumn(name = "user_id", insertable = false, updatable = false)
    private User user;
}
  • insertable = false
    JPA가 INSERT SQL에 해당 컬럼을 포함하지 않음
    즉, 엔티티를 새로 저장할 때 이 컬럼 값은 DB에 반영되지 않음

  • updatable = false
    JPA가 UPDATE SQL에 해당 컬럼을 포함하지 않음
    즉, 엔티티를 수정해도 이 컬럼 값은 변경되지 않음

보통 외래 키를 다른 엔티티나 DB 스키마에서 관리되고 있어
JPA가 중복해서 UPDATE/INSERT 하지 않도록 막고 싶을 때
해당 엔티티를 읽기 전용으로 만들기 위해 사용한다.

1쪽 엔티티에서 @OneToMany(mappedBy = "user")으로 연결

1쪽 엔티티에서 @OneToMany(mappedBy = "user"),
N쪽 엔티티에서 @ManyToOne으로 연결하면
insertable=false, updatable=false 옵션 없이도
양방향 관계 처럼 동작한다.

@Entity
@Table(name = "users")
public class User {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @OneToMany(mappedBy = "user") // Food.user가 관리하는 관계
    private List<Food> foodList = new ArrayList<>();
}

@Entity
@Table(name = "food")
public class Food {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private double price;

    @ManyToOne
    @JoinColumn(name = "user_id") // 외래 키 관리
    private User user;
}

ManyToOne 단방향 관계 ✅

성능/설계 측면에서 @ManyToOne으로 외래 키는 항상 N쪽이 관리하는 게 정석이다.

@Entity
@Table(name = "users")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

}
@Entity
@Table(name = "food")
public class Food {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    private double price;

    // N쪽에서 외래 키 관리
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id") // food 테이블에 user_id FK 생성
    private User user;
}

조회하기

@Test
@DisplayName("1대N 조회 : Food 기준 user 정보 조회")
void test5() {
    Food food = foodRepository.findById(1L).orElseThrow(NullPointerException::new);
    System.out.println("food.getName() = " + food.getName());
    System.out.println("food.getUser().getName() = " + food.getUser().getName());
}

@Test
@DisplayName("N대1 조회 : User 기준 food 정보 조회")
void test6() {
    User user = userRepository.findById(1L).orElseThrow(NullPointerException::new);
    System.out.println("user.getName() = " + user.getName());

    List<Food> foodList = user.getFoodList();
    for (Food food : foodList) {
        System.out.println("food.getName() = " + food.getName());
        System.out.println("food.getPrice() = " + food.getPrice());
    }
}
-- 조회 시 실제로 실행되는 join 쿼리 (User 기준)
select
    u1_0.id,
    u1_0.name,
    f1_0.id,
    f1_0.name,
    f1_0.price

from
	users u1_0
left join
    foods f1_0 
        on u1_0.id=f1_0.user_id 
where
    	u1_0.id=?

N:M

지연 로딩

영속성 전이

고아 Entity 삭제

Git 특강

Git 사용을 하는 동안 stash를 안하고 커밋 되돌리기를 force로 해서 작업 내용을 몽땅 날리는 등 문제가 있었다.
당장 오늘도 Push를 하는데 GitHub 프로필 설정이 잘못 되어 다른 프로필로 커밋이 올려져 고생했다.

나는 git global config의 이메일이 로컬에 표시하는 용도로만 생각했는데, 
gitHub에서는 이 이메일로 GitHub 프로필 정보를 표시한다고 했다.
(근데 검증도 안하고 이래도 되나?!😫)

그런데 오늘 마침 Git 특강을 해주셨다.
질의시간에 이에 대해 여쭤봤는데, --global 옵션을 빼면, git config를 레포지토리 단위로 설정할 수 있다고 하셨다.

Git 구조 이해하기

  • Working Directory
    add
    ↓          ↑
  • Staging Area     |
    commit     checkout
    ↓          |
  • Local Repository
         ↑
    push pull
  • Remote Repository

현재 내가 작업중인 위치를 가리키는 포인터

Git-Flow 브랜치 전략

  • master (or main)
    실제 서비스에 배포되는 최종 릴리즈 버전을 유지하는 브랜치
  • hotfix
    운영 중 긴급한 오류를 수정하기 위한 브랜치로, main에서 분기되어 수정 후 malin과 develop에 반영됨
  • release
    배포를 준비하는 브랜치로, 배포 전 테스트 및 버그 수정을 진행하며 완료 후 main과 - develop에 병합됨
  • develop
    새로운 기능들이 모여 통합되는 개발 중심 브랜치로, feature 브랜치가 병합되는 기준이됨
  • feature
    기능별 브랜치로, develop 브랜치에서 분기되며, 새로운 기능을 개발하기 위한 브랜치로 사용

Git 컨벤션

브랜치 명과 커밋 메시지의 좋은 예

feature

  • feature/티켓번호_add-validation
  • feature/ 티켓번호
  • feat/기능-이름

release

  • release/1,0.0
  • release-1,0,0

hotfix

• hotfix/버전번호_validation
• hotfix-버전번호_validation o fix/기능 이름

develop

  • dev
  • develop

master

  • main
  • master

브랜치명에 /로 구분하는 것은 여러 브랜치가 생성 될 경우 네임스페이스로 구분하기 위함
release, hotix 와 같은 경우는 팀 컨벤션에 따르는 것이 좋다!

Pull Request

Pull Request(PR)는 코드 변경 사항을 다른 브랜치에 병합하도록 요청하는 기능이다.

병합 전 변경 내용을 검토하고 토론하기도 하며 추가로 병합에 필요한 필수 승인자 등을 넣어 배포 승인 절차로 취급할 수도 있다.

PR은 리뷰어들이 PR 을 쉽고 빠르게 이해하기 위해 작은 단위로 올리는 것이 좋다.

코드 작성을 하는 모두가 리뷰어로 적극적으로 참여하는게 좋다.

PR Merge 시 옵션

  • 1. Create a merge commit
    • 머지 커밋을 추가해서 병합
    • 머지 되었다는 기록이 남기 때문에 브랜치 흐름(히스토리) 추적 용이
    • 단점: 히스토리가 지저분해질 수 있음
  • 2. Squash and merge
    • 모든 커밋을 1개로 합쳐 병합
    • 병합 대상 브랜치의 히스토리를 깔끔하게 유지
    • 단점: 합쳐진 브랜치의 작업의 중간 내역이 사라지기 때문에 주의
  • 3. Rebase and merge
    • 병합할 브랜치의 커밋들을 그대로 병합 대상 브랜치 위에 추가
    • 히스토리를 깔끔하게 유지하고 기존 브랜치의 커밋을 보존
    • 단점: 브랜치 분기/병합된 흔적은 사라짐

pull request template

팀에서 PR 메세지를 일관화 하기 위해 템플릿을 사용할 수 있다.
프로젝트 하위에 .github 숨김 폴더에
템플릿내용을 포함한 pull_request_template.md 파일을 생성하면 반영된 브랜치로 pull request를 생성할 때 양식이 자동으로 완성된다.

## 변경 타입
- [ ] 신규 기능 추가/수정
- [ ] 버그 수정
- [ ] 리팩토링
- [ ] 설정
- [ ] 비기능 (주석 등 기능에 영향을 주지 않음)

## 변경 내용
- **as-is**
  - (변경 전 설명을 여기에 작성)

- **to-be**
  - (변경 후 설명을 여기에 작성)

## 코멘트
- (추가적인 설명이나 코멘트가 필요한 경우 여기에 작성)

Pn룰 을 적용한 코멘트 규칙

태그의미리뷰어 의도작성자 권장 행동
P1꼭 반영해주세요⚠️ 중대한 코드 수정이 반드시 필요 (Request changes)요청을 반드시 반영하거나, 불가 시 합리적 이유로 설득
P2적극적으로 고려해주세요⚡ 중요하지만 필수는 아님, 토론 권장 (Request changes)수용하거나, 불가 시 의견을 들어 토론 권장
P3웬만하면 반영해 주세요💬 사소하지만 반영하면 좋은 의견 (Comment)수용하거나, 불가 시 이유 설명 또는 다음 반영 계획(JIRA 티켓 등) 명시
P4반영해도 좋고 넘어가도 좋습니다✅ 중요도가 낮은 의견 (Approve)의견 달지 않아도 무방, 반영 여부 자유
P5그냥 사소한 의견입니다ℹ️ 아주 사소한 의견 (Approve)의견 달지 않아도 무방

Conflict

예) 서로 다른 브랜치에서 같은 파일을 수정하고 서로 main에 병합하려고 할 때
각각의 브랜치에서 수정된 두 파일의 내용을 모두 작성하거나 둘 중 하나만 반영할지 git이 알지 못한다.

이를 선택해서 어떤 커밋의 변경사항을 반영할 지 고르는 것을 충돌을 해결(Resolve Conflict)한다고 한다.

학부1학년때 처음 git을 시작하고 add, commit, push밖에 몰랐던 상태로 팀 프로젝트를 할 때 이러한 충돌이 왜 일어나는지 몰라서 거의 공포로 다가왔던 기억이 있었다.

기억은 잘 안나지만 아마 이 때문에 버전 관리를 안하고 zip파일을 서로 주고받으면서 순서대로 파일을 건드렸던 것 같다.

...버전관리 만세! 우리의 작업시간을 매우 단축시켜줬다.

특강에선 익숙하지 않은 InteliJ GUI에서 충돌을 해결하는 방법 위주로 봤다.

profile
풀스택 연습생. 끈기있는 삽질로 무대에서 화려하게 데뷔할 예정 ❤️🔥

0개의 댓글