[척척학사] Entity 연관관계 분석 및 리팩토링

박상민·2025년 3월 22일
1

척척학사

목록 보기
3/15
post-thumbnail

기존 코드의 문제점

3.22일 기준 엔티티 연관관계가 미흡하면서 발생하는 문제는 아래와 같다.

  • 연관관계로 묶이지 않아 관련 엔티티를 호출하는 과정이 번거롭다
  • 부모 객체 삭제 시 자식 객체도 삭제되도록 하여 효율적으로 관리해야 하지만, 연관관계로 엮여 있지 않아서 수동으로 삭제를 해야한다.
  • 고아 객체가 발생하기 쉽다.

casecade, orphanRemoval 등의 연관관계 옵션을 사용하면 객체를 자동으로 관리할 수 있다. 엔티티 구조를 리팩토링하여 기존에 수동으로 처리하던 부분을 자동화하려고 한다.

Entity 분석

Entity Diagram - 3.22일 기준

User(사용자): X

Student(학생)

  • Department - department/major/secondaryMajor 3개와 각각 ManyToOne

Professor(교수)

  • Department - ManyToOne

Department(학과): X

Course(과목): X

CourseOffering(연도+학기 강의 단위, 과목이 실제 개설된 시점의 정보)

  • Course - ManyToone
  • Department - ManyToOne
  • Professor - ManyToOne

StudentCourse(수강 기록)

  • CourseOffering - ManyToOne
  • Student - ManyToOne
  • points 컬럼 중복 (CourseOffering 엔티티에 points 존재)

StudentAcademicRecord(학생의 누적 성적 요약 정보)

  • Student - OneToOne

SemesterAcademicRecord(특정 학기 성적 요약 정보)

  • Student - ManyToOne

StudentGraduationProgress(학생 졸업 요건 충족 여부)

  • Student - ManyToOne → OneToOne으로 변경

기존 코드

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "student_id", unique = true)
private Student student;
  • 현재의 구조는 unique 설정이 있어서 사실상 1:1 관계이다.
  • 하나의 Student는 하나의 StudentGraduationProgree만 가질 수 있다.
  • 따라서 @OneToOne으로 바꾸는 것을 고려할 수 있다.

변경 코드

@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "student_id")
private Student student;

GraduationRequirement(졸업 요건 기준 - 학과 + 입학년도 기준)

  • Department - ManyToOne
  • 교양, 외국어 인증 등 추가 가능

DepartmentAreaRequirement(졸업 요건 세부 기준)

  • Department - ManyToOne

LiberalArtsAreaCode(교양 분류 코드): X

  • 분류 코드 테이블(classification code table)

Entity 연관관계 리팩토링

User

  • User <-> Student: 1:1 양방향 연관관계로 설정
  • 회원 탈퇴 시 User 엔티티를 삭제하게 되는데 User와 같은 id로 매핑되는 Student 삭제 필요

다만, 이를 응용하려면 User-Student 양방향 연관관계가 설정되어야 하지만 현재 Student 생성을 크롤링 로직에서 담당하고 있다. 추후 Student 생성 로직을 백엔드 코드로 가져오기 전까지는 User 삭제 시 studentId를 사용해 직접 Student를 제거한다.

@Transactional
public void deleteUserByEmail(String email){
    UUID userId = getUserId(email);
    userRepository.deleteById(userId);
    // User - Student 연관관계를 엮어 자동 삭제 로직을 만들어야 하지만, Student 저장 로직이 별도로 존재하지 않아, 연관관계를 형성하지 못함. 수정하기 전까지 임의 삭제 방식으로 진행
    studentRepository.deleteById(userId);
}

User 탈퇴 시 같이 삭제되어야 하는 엔티티

  • Student
    • StudentAcademicRecord
    • SemesterAcademicRecord
    • StudentCourse
    • StudentGraduationProgress

Student

  • 새로운 연관관계 형성
    • StudentAcademicRecord: 1:1
    • SemesterAcademicRecord: 1:M
    • StudentCourse: 1:M
    • StudentGraduationProgress: 1:1

연관관계 설정

@OneToOne(mappedBy = "student", cascade = REMOVE, orphanRemoval = true)
private StudentAcademicRecord studentAcademicRecord;

@OneToOne(mappedBy = "student", cascade = REMOVE, orphanRemoval = true)
private StudentGraduationProgress graduationProgress;

@OneToMany(mappedBy = "student", cascade = REMOVE, orphanRemoval = true)
private List<SemesterAcademicRecord> semesterAcademicRecords = new ArrayList<>();

@OneToMany(mappedBy = "student", cascade = REMOVE, orphanRemoval = true)
private List<StudentCourse> studentCourses = new ArrayList<>();

Student 삭제 시 같이 삭제되어야 하는 정보이기 때문에 cascade REMOVE, orphanRemoval 설정으로 자동 삭제 활성화

  • Student ↔ GraduationRequirement (M:1): 고민 중
    • StudentGraduationProgress가 졸업 요건을 판단할 때 GraduationRequirement를 기준으로 하게 되면, Student or StudentGraduationProgress가 GraduationRequirement를 참조할 수 있어야 한다.
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "graduation_requirement_id")
private GraduationRequirement graduationRequirement;

CourseOffering

  • CourseOffering ↔ LiberalArtsAreaCode (ManyToOne): 논의 필요
    • areaCode가 단순 Integer가 아니라 LiberalArtsAreaCode 테이블 키라면 아래처럼 변경 가능
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "area_code")
private LiberalArtsAreaCode areaCode;

의문점과 해결

GraduationRequirement, DepartmentAreaRequirement 테이블이 따로 존재할 필요가 있을까?

둘은 역할과 범위가 다르기 때문에 분리하는 것이 적절하다.

✅ 두 테이블의 핵심 차이점

왜 분리해야할까?

  • 책임 분리(SRP): GraduationRequirement은 전체 졸업 요건의 기준, DepartmentAreaRequirement는 세부 영역 요건의 기준이기 때문에 역할이 다름
  • 정규화: 하나의 테이블에 총 학점 + 모든 영역 조건을 다 넣으면 테이블 볼륨이 방대하고 중복 데이터 발생
  • 유연성: 영역 요건은 계속 변경될 수 있기 때문에 별도 테이블로 관리하는 게 유리
  • 조회 효율성: ‘과목 영역별 필요 학점 조회’ 같은 경우 빠르게 조회 가능 (areaType 기반 쿼리시)

통합했을 때의 문제점

  • GraduationRequirement 하나에 전핵, 전선, 교양 등을 컬럼으로 넣으면 유연성/확장성이 떨어짐
  • 입학년도/학과에 따라 영역 종류가 다를 수 있기 때문에 정규화가 깨짐

✅ 결론

두 테이블은 졸업 요건 이라는 공통 주제를 다루지만, 역할이 다르기 때문에 분리 설계가 필요하다.

  • GraduationRequirement: 전체 졸업 요건의 총괄 기준 (총 학점, GPA 등)
  • DepartmentAreaRequirement: 영역별 세부 요건 기준 (전공/교양 필수 학점 등)

지금처럼 분리된 두 테이블 구조는 도메인 설계 관점에서도, 데이터베이스 정규화 관점에서도 괜찮은 구조이다.

0개의 댓글