약 3개월 동안 개발에 착수해서 현재 개발이 종료된 FACHIEV프로젝트에 대해서 혼자서 코드 리뷰와 사용했던 스택을 정리하기 위해서 이글을 작성한다! 우선 데이터 베이스 부터 작성하려고 한다.
위에 ERD 에 대해서 명세해 놨으니 참고 바란다 일일이 다 설명하기엔 너무 DB가 거대하다...
개발한 애플리케이션은 패션 전공자들을 위한 패션 깃허브 컨셉의 공간이다. 이점을 참고한다면 분명히 이해할 수 있을 것이다.
@EntityListeners(value={AuditingEntityListener.class})
@MappedSuperclass
@Getter
public abstract class BaseEntity extends BaseTimeEntity {
@CreatedBy
@Column(updatable = false)
private String createdBy;
@LastModifiedBy
private String modifiedBy;
}
@EntityListeners(value={AuditingEntityListener.class})
@MappedSuperclass
@Getter
@Setter
public abstract class BaseTimeEntity {
@CreatedDate
@Column
private LocalDateTime regTime;//등록시간
@LastModifiedDate
private LocalDateTime updateTime;//수정시간
}
위의 두개 클래스는 베이트 엔티티로 사용한 클래스이다. 작성한 사람과 작성된 시간을 알기 위해서 사용했다.
@MappedSuperclass
이 어노테이션은 객체의 입정에서 공통의 매핑을 용하고 싶을 때 사용하는 것이다. DB의 테이블에는 영향을 주지 않으면서 상속해서 사용할 수 있다.
@EnableJpaAuditing
으로 Auditing 기능을 활성화합니다.EntityListener
는 Entity가 DB로 load / persist되기 전후에 커스텀 로직을 선언하는 인터페이스입니다.커스텀 리스너를 구현 후 @EntityListeners
를 통해 해당 엔티티에 등록해줄 수 있는데,이번 예제에서는 Spring에서 제공하는 구현클래스인 AuditingEntityListener
라는 Auditing기능을 수행하는 리스너를 등록했습니다.
AuditingEntityListener
는 해당 엔티티에 선언된 CreatedDate
, LastModifiedDate
, CreatedBy
, LastModifiedBy
어노테이션을탐색해 엔티티 변경 시 해당값을 자동으로 업데이트 해줍니다.
@MappedSuperClass
는 해당 클래스가 상속될 속성을 포함하고 있는 SuperClass 라는걸 알리는 마커 어노테이션입니다.
@Configurable
public class AuditingEntityListener {
private @Nullable ObjectFactory<AuditingHandler> handler;
/**
* Configures the {@linkAuditingHandler} to be used to set the current auditor on the domain types touched.
*
*@paramauditingHandlermust not be {@literalnull}.
*/
public void setAuditingHandler(ObjectFactory<AuditingHandler> auditingHandler) {
Assert.notNull(auditingHandler, "AuditingHandler must not be null!");
this.handler = auditingHandler;
}
/**
* Sets modification and creation date and auditor on the target object in case it implements {@linkAuditable} on
* persist events.
*
*@paramtarget
*/
@PrePersist
public void touchForCreate(Object target) {
Assert.notNull(target, "Entity must not be null!");
if (handler != null) {
AuditingHandler object = handler.getObject();
if (object != null) {
object.markCreated(target);
}
}
}
/**
* Sets modification and creation date and auditor on the target object in case it implements {@linkAuditable} on
* update events.
*
*@paramtarget
*/
@PreUpdate
public void touchForUpdate(Object target) {
Assert.notNull(target, "Entity must not be null!");
if (handler != null) {
AuditingHandler object = handler.getObject();
if (object != null) {
object.markModified(target);
}
}
}
}
위의 인터페이스를 참고바란다.
package com.facaieve.backend.entity.user;
import com.facaieve.backend.Constant.UserRole;
import com.facaieve.backend.entity.basetime.BaseEntity;
import com.facaieve.backend.Constant.UserActive;
import com.facaieve.backend.entity.comment.FashionPickUpCommentEntity;
import com.facaieve.backend.entity.comment.FundingCommentEntity;
import com.facaieve.backend.entity.comment.PortfolioCommentEntity;
import com.facaieve.backend.entity.etc.MyPickEntity;
import com.facaieve.backend.entity.image.ImageEntityProfile;
import com.facaieve.backend.entity.image.S3ImageInfo;
import com.facaieve.backend.entity.post.FashionPickupEntity;
import com.facaieve.backend.entity.post.FundingEntity;
import com.facaieve.backend.entity.post.PortfolioEntity;
import javax.persistence.*;
import javax.transaction.Transactional;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import org.hibernate.annotations.ColumnDefault;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.io.Serializable;
import java.util.*;
@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Table(uniqueConstraints = {@UniqueConstraint(columnNames = "email")})// 이메일 기준으로 사용자 구분
public class UserEntity extends BaseEntity implements UserDetails, Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)//개별 엔티티 적용
@Schema(description = "유저 식별ID")
Long userEntityId;
@Schema(description = "유저 권한")
@Enumerated(EnumType.STRING)
@Column
UserRole role;//todo관리자와 일반 사용자를 구분하는 로직을 구현할 것
@Schema(description = "유저 닉네임")
@Column
String displayName;
@Schema(description = "유저 이메일")
@Column
String email;
@Schema(description = "비밀번호")
@Column
String password;
@Schema(description = "광역시, 도")
@Column
String state;
@Schema(description = "시,군,구")
@Column
String city;
@Schema(description = "간단한 자기소개")
@Column
String userInfo;
@Schema(description = "커리어")
@Column
String career;
@Schema(description = "학력 및 교육사항")
@Column
String education;
@Schema(description = "재직회사")
@Column
String Company;
@Schema(description = "유저 활동 상태")
@Enumerated(value = EnumType.STRING)
UserActive userActive = UserActive.Active;
@Schema(description = "이메일 인증 상태", defaultValue = "false")
private boolean emailVerified = false;
//패션픽업 댓글, 펀딩 댓글, 포폴 댓글 엔티티 매핑
@Schema(description = "패션 픽업 게시물에 단 댓글 목록")
@OneToMany(mappedBy = "userEntity", cascade = CascadeType.PERSIST)
List<FashionPickUpCommentEntity> fashionPickUpCommentEntities = new ArrayList<FashionPickUpCommentEntity>();
@Schema(description = "펀딩 게시물에 단 댓글 목록")
@OneToMany(mappedBy = "userEntity", cascade = CascadeType.PERSIST)
List<FundingCommentEntity> fundingCommentEntities = new ArrayList<FundingCommentEntity>();
@Schema(description = "포트폴리오 게시물에 단 댓글 목록")
@OneToMany(mappedBy = "userEntity", cascade = CascadeType.PERSIST)
List<PortfolioCommentEntity> portfolioCommentEntities = new ArrayList<PortfolioCommentEntity>();
//패션픽업, 펀딩, 포폴 엔티티 매핑
@Schema(description = "작성한 패션 픽업 게시물 목록")
@OneToMany(mappedBy = "userEntity", cascade = CascadeType.PERSIST)
List<FashionPickupEntity> fashionPickupEntities = new ArrayList<FashionPickupEntity>();
@Schema(description = "작성한 펀딩 게시물 목록")
@OneToMany(mappedBy = "userEntity", cascade = CascadeType.PERSIST)
List<FundingEntity> fundingEntities = new ArrayList<FundingEntity>();
@Schema(description = "작성한 포트폴리오 게시물 목록")
@OneToMany(mappedBy = "userEntity", cascade = CascadeType.PERSIST)
List<PortfolioEntity> portfolioEntities = new ArrayList<PortfolioEntity>();
@OneToMany(mappedBy = "followingUserEntity", cascade = CascadeType.ALL)
List<FollowEntity> followingList = new ArrayList<FollowEntity>(); // 팔로우 정보 저장을 위한 셀프 참조
@OneToMany(mappedBy = "followedUserEntity", cascade = CascadeType.ALL)
List<FollowEntity> followerList = new ArrayList<FollowEntity>(); // 팔로우 정보 저장을 위한 셀프 참조
@Schema(description = "프로필 이미지")
@OneToOne(mappedBy = "userEntity", cascade = {CascadeType.PERSIST, CascadeType.REMOVE})
S3ImageInfo profileImg; // 프로필 이미지 매핑
@Schema(description = "마이픽을 설정한 게시물 및 댓글")
@OneToMany(mappedBy = "pickingUser", cascade = CascadeType.PERSIST)
List<MyPickEntity> myPickEntityList = new ArrayList<MyPickEntity>();
//사진과 이름을 업데이트함
public UserEntity update(String displayName, String picture ){
this.displayName = displayName;
this.profileImg = S3ImageInfo.builder()
.userEntity(this)
.fileURI(picture)
.build();
return this;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority(role.getUserRole()));//권한 반환
}
@Override
public String getUsername() {
return email;
}//위에서 email 을 제약 조건으로 설정했기 때문에 email 을 이름으로 반환함.
@Override
public boolean isAccountNonExpired() {
return this.userActive.equals("활동 상태");
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
public void emailVerifiedSuccess(){//email 인증을 위한
this.emailVerified = true;
}
}
@Schema(description = "작성한 포트폴리오 게시물 목록")
@OneToMany(mappedBy = "userEntity", cascade = CascadeType.PERSIST)
List<PortfolioEntity> portfolioEntities = new ArrayList<PortfolioEntity>();
이 코드가 사실 전부이다. 패션 게시글에 대한 정보를 담고 있는 리스트를 가지고 있어서 마이페이지에서 조회가능하도록 구현했다. @OneToMany 어노테이션을 사용해서 일대다 관계를 맵핑했다 또한 CascadeType.PERSIST 를 사용해서 자식객체에 동시에 영속성을 부여했다.