[프로젝트] 개발자 포트폴리오 - 7. [백엔드] 포트폴리오 데이터 모델링

개발자 포트폴리오 프로젝트

7. [백엔드] 포트폴리오 데이터 모델링

전체 소스 - https://github.com/vividswan/Portfolio-For-Developers


Entity Relationship Diagram(ERD)

1

위와 같은 관계를 갖는 테이블들을 Entity 객체들로 구현해보자.

Enum 및 Enum 사용 Entity 생성

source commit - d1cffe9

DType.java

package com.portfolio.backend.portfolio.type;

public enum DType {
    P, C, S
}

Career를 상속받는 Project, Company, School 객체를 위한 DType Enum이다.
컨트롤러에서 세 개의 객체를 구별하기 위해 사용할 예정이다.

TechType.java

package com.portfolio.backend.portfolio.type;

public enum TechType {
    HTML(100L), CSS(101L), JavaScript(102L), jQuery(103L),
    ReactJs(104L), ZeptoJs(105L), Angular(106L), VueJs(107L),
    BackBone(108L), Ember(109L), Ruby(110L),RubyOnRails(111L), Python(112L),
    Django(113L), Flask(114L), Pylons(115L),PHP(116L), Laravel(117L), Java(118L),
    Spring(119L), SpringBoot(120L), Scala(121L), Play(122L), NodeJS(123L),
    MySQL(124L), PostgreSQL(125L), MongoDB(126L), RelationalSQL(127L), NonRelationalSQL(128L),
    Apache(129L), Nginx(130L), Bootstrap(131L), redis(132L), AWS(133L), Enzyme(134L), Chai(135L),
    Travis(136L), GitLabCI(137L), BitBucketPipeLine(138L), Jenkins(139L), GCP(140L), GoogleCloudPlatform(141L);

    private Long value;

    private TechType(Long value){
        this.value = value;
    }

    public Long getValue() {
        return this.value;
    }
}

포트폴리오와 프로젝트에서 고를 수 있는 기술 스택을 Enum으로 선언해 준다.
Long value를 선언하여 각 기술 스택마다 고유 번호를 주었다.
고유 번호는 추후 백엔드와 프론트엔드에서 사용할 예정이다.

TechStack.java

package com.portfolio.backend.portfolio.entitiy;

// import 생략

@Builder
@AllArgsConstructor
@Getter
@Entity
public class TechStack {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Enumerated(EnumType.STRING)
    @Column(name = "tech_type")
    private TechType techType;

    protected TechStack(){}

    public TechStack(Portfolio portfolio, TechType techType){
        this.portfolio = portfolio;
        this.techType = techType;
    }

    public TechStack(Project project, TechType techType){
        this.project = project;
        this.techType = techType;
    }


    @ManyToOne
    @JoinColumn(name = "portfolio_id")
    private Portfolio portfolio;

    @ManyToOne
    @JoinColumn(name = "project_id")
    private Project project;

}

TechType Enum을 칼럼으로 사용하는 TechStack Entity 객체를 만들어주었다.
포트폴리오 테이블, 프로젝트 테이블과 양방향 매핑을 할 것이며, 다대일 어노테이션(ManyToOne)을 선언했다.

Career 객체와 Career를 상속받는 객체 생성

source commit - f8835ef

Career Entity 객체를 만들어주고, 이를 상속받는 객체들인 Project, School, Company를 만들어준다.

Career.java

package com.portfolio.backend.portfolio.entitiy.career;

// ... import 생략

@Getter
@Entity
@AllArgsConstructor
@NoArgsConstructor
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn
public abstract class Career extends BaseTimeEntity {

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

    @JsonBackReference
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "portfolio_id")
    private Portfolio portfolio;

    @JsonBackReference
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "account_id")
    private Account account;

    @Column(nullable = false)
    private String name;

    @Column(nullable = false)
    private LocalDate startDate;

    private LocalDate endDate;

    private String contents;

    public Career(Portfolio portfolio,
                  Account account,
                  String name,
                  LocalDate startDate,
                  LocalDate endDate,
                  String contents){

        this.name = name;
        this.portfolio = portfolio;
        this.account = account;
        this.startDate = startDate;
        this.endDate = endDate;
        this.contents = contents;
    }

    public void updateCareer(String name, String contents,LocalDate startDate, LocalDate endDate){
        this.name = name;
        this.contents = contents;
        this.startDate = startDate;
        this.endDate = endDate;
    }
}

Project, Company, School에 상속시켜줄 Career 객체이다.

@Inheritance(strategy = InheritanceType.JOINED)

상속관계 매핑 전략 중 JOINED 전략을 사용했다.
JOINED 전략을 사용하면 상속하는 객체가 갖고 있는 공통의 속성은 Career 테이블에만 저장되고, 구별되는 데이터는 각각의 테이블에 저장된다.
이 전략대로 생성하면, 슈퍼 타입과, 서브 타입 관계를 갖고 있는 테이블을 생성한다.

@DiscriminatorColumn

@DiscriminatorColumn 어노테이션을 선언해야 Career 테이블에 DType 칼럼을 생성해 준다.
Project, Company, School의 구별을 명확하게 해주기 위해 이 어노테이션을 추가해 주자.

@JsonBackReference
@ManyToOne(fetch = FetchType.LAZY)

@ManyToOne 을 이용해 Portfolio, Account 객체와 다대일 관계를 맺을 것이며, fetch 전략은 Lazy 전략을 이용할 것이다.
프로젝트 하나하나마다 방대한 크기의 포트폴리오와 계정 정보를 들고 올 필요가 없기 때문이다.
@JsonBackReference은 Lazy fetch 때 발생할 수 있는 프록시 호출을 방지하기 위해 추가하였다.

public Career(Portfolio portfolio,
                  Account account,
                  String name,
                  LocalDate startDate,
                  LocalDate endDate,
                  String contents)

상속받는 Entity는 Builder 패턴을 활용해서 객체를 생성할 건데, 그때 사용하기 위해 부모 Entity가 가지고 있는 필드들에 값을 넣어주는 생성자를 만들었다.

public void updateCareer

이 메소드 또 한 상속받는 Entity에서 사용할 비즈니스 로직을 위해 만들었다.

Company.java

package com.portfolio.backend.portfolio.entitiy.career;

// ... import 생략

@Getter
@AllArgsConstructor
@NoArgsConstructor
@DiscriminatorValue("C")
@Entity
public class Company extends Career {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String department;

    @Builder
    private Company(
            Portfolio portfolio,
            Account account,
            String name,
            LocalDate startDate,
            LocalDate endDate,
            String contents,
            String department
    ){
                    super(portfolio, account, name, startDate, endDate, contents);
                    this.department = department;
    }


    public void updateProject(CompanyDataRequest dto){
        super.updateCareer(dto.getName(),dto.getContents(),dto.getStartDate(), dto.getEndDate());
        this.department = dto.getDepartment();
    }
}

Project.java

package com.portfolio.backend.portfolio.entitiy.career;

// ... import 생략

@Getter
@AllArgsConstructor
@NoArgsConstructor
@DiscriminatorValue("P")
@Entity
public class Project extends Career {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String url;

    @OneToMany(mappedBy = "project")
    private Set<TechStack>  techStacks = new HashSet<>();

    @Builder
    private Project(
            Portfolio portfolio,
            Account account,
            String name,
            LocalDate startDate,
            LocalDate endDate,
            String contents,
            String url
    ){
                    super(portfolio, account, name, startDate, endDate, contents);
                    this.url = url;
    }


    public void updateProject(ProjectDataRequest dto){
        super.updateCareer(dto.getName(),dto.getContents(),dto.getStartDate(), dto.getEndDate());
        this.url = dto.getUrl();
    }
}

School.java

package com.portfolio.backend.portfolio.entitiy.career;

// ... import 생략

@Getter
@AllArgsConstructor
@NoArgsConstructor
@DiscriminatorValue("S")
@Entity
public class School extends Career {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String major;
    private boolean graduate;

    @Builder
    private School(
            Portfolio portfolio,
            Account account,
            String name,
            LocalDate startDate,
            LocalDate endDate,
            String contents,
            String major,
            boolean graduate
    ){
                    super(portfolio, account, name, startDate, endDate, contents);
                    this.major = major;
                    this.graduate = graduate;
    }


    public void updateProject(SchoolDataRequest dto){
        super.updateCareer(dto.getName(),dto.getContents(),dto.getStartDate(), dto.getEndDate());
        this.major = dto.getMajor();
        this.graduate = dto.isGraduate();
    }
}

Career를 상속받는 세 가지 Entity 객체들이다.

@DiscriminatorValue 어노테이션으로 테이블에 들어갈 DType 값을 선언해 준다.
Career에서 선언한 생성자와 메소드를 Builder 패턴과 업데이트 메서드에 super로 적용해 준다.

Portfolio Entity 객체 생성 및 Account와 관련 된 세팅 추가

source commit - bde35e7

Portfolio.java

package com.portfolio.backend.portfolio.entitiy;

// ... import 생략

@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
@Entity
public class Portfolio extends BaseTimeEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @OneToOne
    @JoinColumn(name = "account_id")
    private Account account;

    @Column(nullable = false, unique = true)
    private String accountNickname;

    private String name;

    private String introduction;

    @Column(nullable = false, unique = true)
    private String email;

    @Column(unique = true)
    private String gitId;

    @Column(unique = true)
    private String bojId;

    @Column(unique = true)
    private String blogUrl;

    private String occupation;

    private String location;

    @Lob
    private String profileImage;

    @OneToMany(mappedBy = "portfolio", cascade =  CascadeType.REMOVE)
    @JsonIgnoreProperties({"portfolio"})
    private Set<TechStack> techStacks = new HashSet<>();

    @OneToMany(mappedBy = "portfolio", cascade =  CascadeType.REMOVE)
    @JsonIgnoreProperties({"portfolio"})
    private List<Project> projects = new ArrayList<>();

    @OneToMany(mappedBy = "portfolio", cascade =  CascadeType.REMOVE)
    @JsonIgnoreProperties({"portfolio"})
    private List<School> schools = new ArrayList<>();

    @OneToMany(mappedBy = "portfolio", cascade =  CascadeType.REMOVE)
    @JsonIgnoreProperties({"portfolio"})
    private List<Company> companies = new ArrayList<>();

    public void updatePortInfo(PortUpdateRequest dto){
        this.name = dto.getName();
        this.introduction = dto.getIntroduction();
        this.gitId = dto.getGitId();
        this.blogUrl = dto.getBlogUrl();
        this.occupation = dto.getOccupation();
        this.location = dto.getLocation();
        this.profileImage = dto.getProfileImage();
    }

}

Portfolo에 필요한 속성들과 일대다로 선언 뒤 조회를 할 컬렉션들을 선언했다.

연관관계의 주인

@OneToOne
@JoinColumn(name = "account_id")
private Account account;

하나의 계정 당 하나의 포트폴리오를 만들 것이기 때문에 Account 객체와는 일대일 매핑 관계로 선언하였다.
추후 개발에 Account - Portfolio가 일대다 관계, 즉 하나의 계정 당 여러 개의 포트폴리오를 만들게 될 가능성을 염두하여 연관관계의 주인은 Portfolio 객체로 설정했다.

updatePortInfo 메소드는 Portfolio 객체에 있는 필드값만 수정해 주는 메소드다.

Account와 관련 된 세팅 추가

Account.java


// ...
@OneToOne(mappedBy = "account")
    private Portfolio portfolio;
// ...

Account Entity에 Portfolio 칼럼을 추가해 준다.
연관관계의 주인이 아니므로 mappedBy로 선언한다.

AccountService.java

// ...
        Portfolio portfolio = Portfolio.builder()
                .email(dto.getEmail())
                .accountNickname(dto.getNickname())
                .build();

        portfolioRepository.save(portfolio);
// ...

AccountService 객체의 signUp 메소드에서
accountRepository.save(account);
로 Account가 생성되면 바로 그 계정과 일대일 매핑되는 포트폴리오를 생성한 뒤 저장해 준다.
처음의 포트폴리오엔 계정 주인의 이메일과 닉네임만 들어간다.


오류 수정

  • 포트폴리오 생성 시, Account 정보를 포트폴리오에 담지 않아서 이를 수정하고, 양방향 참조와 Lazy fetch에 의한 프록시 객체 호출 오류를 수정했다.
    source commit - 49d7165

  • 포트폴리오 정보와 계정 정보를 @ManyToOne으로 Career에 담았으나, 각각 상속받은 객체를 찾는 과정에 문제가 생겨서 각각의 포트폴리오 정보와 계정 정보에 대한 소스를 상속받은 객체들에게 추가 시켰다. Career 객체에는 매핑에 대한 필드는 지우고, 계정 닉네임 칼럼을 추가했다.
    source commit - cca8ba7

  • TechStack Entity에 LAZY fetch 옵션을 추가하고, Project Entity에 무한 참조를 방지하는 코드를 추가하였다.(@JsonIgnoreProperties) 또한, TechStack Entity를 Project나 Portfolio에서 참조할 때 다시 두 객체가 리턴되지 않도록 protected로 은닉화하였다.
    source commit - 884bd4c