diff --git a/.gitignore b/.gitignore index c2065bc26..5ca2add7d 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,8 @@ out/ ### VS Code ### .vscode/ + +### Custom ### +db_dev.mv.db +db_dev.trace.db +.env \ No newline at end of file diff --git a/src/main/java/com/back/BackApplication.java b/src/main/java/com/back/BackApplication.java index e5e900c6b..363f38d70 100644 --- a/src/main/java/com/back/BackApplication.java +++ b/src/main/java/com/back/BackApplication.java @@ -2,7 +2,9 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +@EnableJpaAuditing // 테이블에 로우데이터가 쌓일 때 수정일/생성일이 자동으로 갱신된다. @SpringBootApplication public class BackApplication { diff --git a/src/main/java/com/back/boundedContext/member/app/MemberService.java b/src/main/java/com/back/boundedContext/member/app/MemberService.java new file mode 100644 index 000000000..95bbd3a44 --- /dev/null +++ b/src/main/java/com/back/boundedContext/member/app/MemberService.java @@ -0,0 +1,38 @@ +package com.back.boundedContext.member.app; + + +import com.back.boundedContext.member.domain.Member; +import com.back.global.exception.DomainException; +import com.back.boundedContext.member.out.MemberRepository; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +public class MemberService { + private final MemberRepository memberRepository; + + public MemberService(MemberRepository memberRepository) { + this.memberRepository = memberRepository; + } + + public long count() { + return memberRepository.count(); + } + + public Member join(String username, String password, String nickname) { + findByUsername(username).ifPresent(m -> { + throw new DomainException("409-1", "이미 존재하는 username 입니다."); + }); + + return memberRepository.save(new Member(username, password, nickname)); + } + + public Optional findByUsername(String username) { + return memberRepository.findByUsername(username); + } + + public Optional findById(int id) { + return memberRepository.findById(id); + } +} diff --git a/src/main/java/com/back/boundedContext/member/domain/Member.java b/src/main/java/com/back/boundedContext/member/domain/Member.java new file mode 100644 index 000000000..0e2d61652 --- /dev/null +++ b/src/main/java/com/back/boundedContext/member/domain/Member.java @@ -0,0 +1,30 @@ +package com.back.boundedContext.member.domain; + +import com.back.global.jpa.entity.BaseIdAndTime; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@NoArgsConstructor +@Getter +public class Member extends BaseIdAndTime { + @Column(unique = true) + private String username; + private String password; + private String nickname; + private int activityScore; + + public Member(String username, String password, String nickname) { + this.username = username; + this.password = password; + this.nickname = nickname; + } + + public int increaseActivityScore(int amount) { + return this.activityScore+=amount; + } + + +} diff --git a/src/main/java/com/back/boundedContext/member/in/MemberEventListener.java b/src/main/java/com/back/boundedContext/member/in/MemberEventListener.java new file mode 100644 index 000000000..aa7793d79 --- /dev/null +++ b/src/main/java/com/back/boundedContext/member/in/MemberEventListener.java @@ -0,0 +1,36 @@ +package com.back.boundedContext.member.in; + +import com.back.boundedContext.member.domain.Member; +import com.back.boundedContext.member.app.MemberService; +import com.back.shared.post.event.PostCommentCreatedEvent; +import com.back.shared.post.event.PostCreatedEvent; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.event.TransactionalEventListener; + +import static org.springframework.transaction.annotation.Propagation.REQUIRES_NEW; +import static org.springframework.transaction.event.TransactionPhase.AFTER_COMMIT; + + +@Component +@RequiredArgsConstructor +public class MemberEventListener { + private final MemberService memberService; + + @TransactionalEventListener(phase = AFTER_COMMIT) + @Transactional(propagation = REQUIRES_NEW) + public void handle(PostCreatedEvent event) { + Member member = memberService.findById(event.getPost().getAuthorId()).get(); + + member.increaseActivityScore(3); + } + + @TransactionalEventListener(phase = AFTER_COMMIT) + @Transactional(propagation = REQUIRES_NEW) + public void handle(PostCommentCreatedEvent event) { + Member member = memberService.findById(event.getPostComment().getAuthorId()).get(); + + member.increaseActivityScore(1); + } +} diff --git a/src/main/java/com/back/boundedContext/member/out/MemberRepository.java b/src/main/java/com/back/boundedContext/member/out/MemberRepository.java new file mode 100644 index 000000000..e51c38d27 --- /dev/null +++ b/src/main/java/com/back/boundedContext/member/out/MemberRepository.java @@ -0,0 +1,12 @@ +package com.back.boundedContext.member.out; + +import com.back.boundedContext.member.domain.Member; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface MemberRepository extends JpaRepository { + Optional findByUsername(String username); + + Optional findById(int id); +} diff --git a/src/main/java/com/back/boundedContext/post/app/PostService.java b/src/main/java/com/back/boundedContext/post/app/PostService.java new file mode 100644 index 000000000..2e95cf083 --- /dev/null +++ b/src/main/java/com/back/boundedContext/post/app/PostService.java @@ -0,0 +1,39 @@ +package com.back.boundedContext.post.app; + + +import com.back.boundedContext.member.domain.Member; +import com.back.boundedContext.post.domain.Post; +import com.back.boundedContext.post.out.PostRepository; +import com.back.global.EventPublisher.EventPublisher; +import com.back.shared.dto.PostDto; +import com.back.shared.post.event.PostCreatedEvent; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class PostService { + private final PostRepository postRepository; + private final EventPublisher eventPublisher; + + public long count() { + return postRepository.count(); + } + + public Post write(Member author, String title, String content) { + Post post = postRepository.save(new Post(author, title, content)); + + eventPublisher.publish( + new PostCreatedEvent( + new PostDto(post) + ) + ); + + return post; + } + public Optional findById(int id) { + return postRepository.findById(id); + } +} diff --git a/src/main/java/com/back/boundedContext/post/domain/Post.java b/src/main/java/com/back/boundedContext/post/domain/Post.java new file mode 100644 index 000000000..76bcb6a55 --- /dev/null +++ b/src/main/java/com/back/boundedContext/post/domain/Post.java @@ -0,0 +1,53 @@ +package com.back.boundedContext.post.domain; + +import com.back.boundedContext.member.domain.Member; +import com.back.global.jpa.entity.BaseIdAndTime; +import com.back.shared.dto.PostCommentDto; +import com.back.shared.post.event.PostCommentCreatedEvent; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +import static jakarta.persistence.CascadeType.PERSIST; +import static jakarta.persistence.CascadeType.REMOVE; +import static jakarta.persistence.FetchType.LAZY; + +@Entity +@NoArgsConstructor +@Getter +public class Post extends BaseIdAndTime { + + @ManyToOne(fetch = LAZY) //지연 로딩 + private Member author; + private String title; + @Column(columnDefinition = "LONGTEXT") + private String content; + + @OneToMany(mappedBy = "post",cascade = {PERSIST, REMOVE}, orphanRemoval = true) // 영속성 전이 및 고아 객체 제거 + private List comments = new ArrayList<>(); + + public Post(Member author, String title, String content) { + this.author = author; + this.title = title; + this.content = content; + } + + public PostComment addComment(Member author, String content) { + PostComment postComment = new PostComment(this, author, content); + + comments.add(postComment); + publishEvent(new PostCommentCreatedEvent(new PostCommentDto(postComment))); + + return postComment; + } + + public boolean hasComments() { + return !comments.isEmpty(); + } +} diff --git a/src/main/java/com/back/boundedContext/post/domain/PostComment.java b/src/main/java/com/back/boundedContext/post/domain/PostComment.java new file mode 100644 index 000000000..b6862e286 --- /dev/null +++ b/src/main/java/com/back/boundedContext/post/domain/PostComment.java @@ -0,0 +1,29 @@ +package com.back.boundedContext.post.domain; + +import com.back.boundedContext.member.domain.Member; +import com.back.global.jpa.entity.BaseIdAndTime; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.ManyToOne; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import static jakarta.persistence.FetchType.LAZY; + +@Entity +@NoArgsConstructor +@Getter +public class PostComment extends BaseIdAndTime { + @ManyToOne(fetch = LAZY) + private Post post; + @ManyToOne(fetch = LAZY) + private Member author; + @Column(columnDefinition = "TEXT") + private String content; + + public PostComment(Post post, Member author, String content) { + this.post = post; + this.author = author; + this.content = content; + } +} diff --git a/src/main/java/com/back/boundedContext/post/out/PostRepository.java b/src/main/java/com/back/boundedContext/post/out/PostRepository.java new file mode 100644 index 000000000..0dd65b3c4 --- /dev/null +++ b/src/main/java/com/back/boundedContext/post/out/PostRepository.java @@ -0,0 +1,7 @@ +package com.back.boundedContext.post.out; + +import com.back.boundedContext.post.domain.Post; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PostRepository extends JpaRepository { +} diff --git a/src/main/java/com/back/global/EventPublisher/EventPublisher.java b/src/main/java/com/back/global/EventPublisher/EventPublisher.java new file mode 100644 index 000000000..56945caa3 --- /dev/null +++ b/src/main/java/com/back/global/EventPublisher/EventPublisher.java @@ -0,0 +1,17 @@ +package com.back.global.EventPublisher; + + +import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class EventPublisher { + private final ApplicationEventPublisher applicationEventPublisher; + + public void publish(Object event){ + applicationEventPublisher.publishEvent(event); + } + +} diff --git a/src/main/java/com/back/global/exception/DomainException.java b/src/main/java/com/back/global/exception/DomainException.java new file mode 100644 index 000000000..298714f23 --- /dev/null +++ b/src/main/java/com/back/global/exception/DomainException.java @@ -0,0 +1,15 @@ +package com.back.global.exception; + +import lombok.Getter; + +@Getter +public class DomainException extends RuntimeException { + private final String resultCode; + private final String msg; + + public DomainException(String resultCode, String msg) { + super(resultCode + " : " + msg); + this.resultCode = resultCode; + this.msg = msg; + } +} diff --git a/src/main/java/com/back/global/global/GlobalConfig.java b/src/main/java/com/back/global/global/GlobalConfig.java new file mode 100644 index 000000000..1724a57e1 --- /dev/null +++ b/src/main/java/com/back/global/global/GlobalConfig.java @@ -0,0 +1,18 @@ +package com.back.global.global; + + +import com.back.global.EventPublisher.EventPublisher; +import lombok.Getter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class GlobalConfig { + @Getter + private static EventPublisher eventPublisher; + + @Autowired + public void setEventPublisher(EventPublisher eventPublisher) { + GlobalConfig.eventPublisher = eventPublisher; + } +} diff --git a/src/main/java/com/back/global/initData/DataInit.java b/src/main/java/com/back/global/initData/DataInit.java new file mode 100644 index 000000000..2a0a3c38e --- /dev/null +++ b/src/main/java/com/back/global/initData/DataInit.java @@ -0,0 +1,95 @@ +package com.back.global.initData; + +import com.back.boundedContext.member.domain.Member; +import com.back.boundedContext.post.domain.Post; +import com.back.boundedContext.member.app.MemberService; +import com.back.boundedContext.post.app.PostService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.ApplicationRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; +import org.springframework.transaction.annotation.Transactional; + +@Configuration +@Slf4j +public class DataInit { // 실행될 때 만들어지는 데이터. 테스트용으로 좋다 + private final DataInit self; + private final MemberService memberService; + private final PostService postService; + + public DataInit( + @Lazy DataInit self, + MemberService memberService, + PostService postService + ) { + this.self = self; + this.memberService = memberService; + this.postService = postService; + } + + @Bean + public ApplicationRunner baseInitDataRunner() { //ApplicationRunner Bean을 등록하면 스프링부트가 시작할 때 한 번 실행됨 + return args -> { + self.makeBaseMembers(); + self.makeBasePosts(); + self.makeBasePostComments(); + }; + } + + @Transactional + public void makeBaseMembers() { + if (memberService.count() > 0) return; // 데이터가 이미 있으면 return + + Member systemMember = memberService.join("system", "1234", "시스템"); + Member holdingMember = memberService.join("holding", "1234", "홀딩"); + Member adminMember = memberService.join("admin", "1234", "관리자"); + Member user1Member = memberService.join("user1", "1234", "유저1"); + Member user2Member = memberService.join("user2", "1234", "유저2"); + Member user3Member = memberService.join("user3", "1234", "유저3"); + } + + @Transactional + public void makeBasePosts() { + if (postService.count() > 0) return; + + Member user1Member = memberService.findByUsername("user1").get(); + Member user2Member = memberService.findByUsername("user2").get(); + Member user3Member = memberService.findByUsername("user3").get(); + + Post post1 = postService.write(user1Member, "제목1", "내용1"); + Post post2 = postService.write(user1Member, "제목2", "내용2"); + Post post3 = postService.write(user1Member, "제목3", "내용3"); + Post post4 = postService.write(user2Member, "제목4", "내용4"); + Post post5 = postService.write(user2Member, "제목5", "내용5"); + Post post6 = postService.write(user3Member, "제목6", "내용6"); + } + @Transactional + public void makeBasePostComments() { + Post post1 = postService.findById(1).get(); + Post post2 = postService.findById(2).get(); + Post post3 = postService.findById(3).get(); + Post post4 = postService.findById(4).get(); + Post post5 = postService.findById(5).get(); + Post post6 = postService.findById(6).get(); + + Member user1Member = memberService.findByUsername("user1").get(); + Member user2Member = memberService.findByUsername("user2").get(); + Member user3Member = memberService.findByUsername("user3").get(); + + if (post1.hasComments()) return; + + post1.addComment(user1Member, "댓글1"); + post1.addComment(user2Member, "댓글2"); + post1.addComment(user3Member, "댓글3"); + + post2.addComment(user2Member, "댓글4"); + post2.addComment(user2Member, "댓글5"); + + post3.addComment(user3Member, "댓글6"); + post3.addComment(user3Member, "댓글7"); + + post4.addComment(user1Member, "댓글8"); + } + +} diff --git a/src/main/java/com/back/global/jpa/entity/BaseEntity.java b/src/main/java/com/back/global/jpa/entity/BaseEntity.java new file mode 100644 index 000000000..a618d88b2 --- /dev/null +++ b/src/main/java/com/back/global/jpa/entity/BaseEntity.java @@ -0,0 +1,21 @@ +package com.back.global.jpa.entity; + +import com.back.global.global.GlobalConfig; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import lombok.Getter; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +@Getter +// 모든 엔티티들의 조상 +public class BaseEntity { + public String getModelTypeCode() { + return this.getClass().getSimpleName(); + } + + protected void publishEvent(Object event) { + GlobalConfig.getEventPublisher().publish(event); + } +} diff --git a/src/main/java/com/back/global/jpa/entity/BaseIdAndTime.java b/src/main/java/com/back/global/jpa/entity/BaseIdAndTime.java new file mode 100644 index 000000000..6e2a16edf --- /dev/null +++ b/src/main/java/com/back/global/jpa/entity/BaseIdAndTime.java @@ -0,0 +1,28 @@ +package com.back.global.jpa.entity; + + +import jakarta.persistence.EntityListeners; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.MappedSuperclass; +import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +import static jakarta.persistence.GenerationType.IDENTITY; + +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +@Getter +public class BaseIdAndTime extends BaseEntity { + @Id + @GeneratedValue(strategy = IDENTITY) + private int id; + @CreatedDate + private LocalDateTime createDate; + @LastModifiedDate + private LocalDateTime modifyDate; +} diff --git a/src/main/java/com/back/shared/dto/PostCommentDto.java b/src/main/java/com/back/shared/dto/PostCommentDto.java new file mode 100644 index 000000000..8ba5a0d26 --- /dev/null +++ b/src/main/java/com/back/shared/dto/PostCommentDto.java @@ -0,0 +1,31 @@ +package com.back.shared.dto; + +import com.back.boundedContext.post.domain.PostComment; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.time.LocalDateTime; + +@AllArgsConstructor +@Getter +public class PostCommentDto { + private final int id; + private final LocalDateTime createDate; + private final LocalDateTime modifyDate; + private final int postId; + private final int authorId; + private final String authorName; + private final String content; + + public PostCommentDto(PostComment comment) { + this( + comment.getId(), + comment.getCreateDate(), + comment.getModifyDate(), + comment.getPost().getId(), + comment.getAuthor().getId(), + comment.getAuthor().getNickname(), + comment.getContent() + ); + } +} diff --git a/src/main/java/com/back/shared/dto/PostDto.java b/src/main/java/com/back/shared/dto/PostDto.java new file mode 100644 index 000000000..1826f847e --- /dev/null +++ b/src/main/java/com/back/shared/dto/PostDto.java @@ -0,0 +1,31 @@ +package com.back.shared.dto; + +import com.back.boundedContext.post.domain.Post; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.time.LocalDateTime; + +@AllArgsConstructor +@Getter +public class PostDto { + private final int id; + private final LocalDateTime createDate; + private final LocalDateTime modifyDate; + private final int authorId; + private final String authorName; + private final String title; + private final String content; + + public PostDto(Post post) { + this( + post.getId(), + post.getCreateDate(), + post.getModifyDate(), + post.getAuthor().getId(), + post.getAuthor().getNickname(), + post.getTitle(), + post.getContent() + ); + } +} diff --git a/src/main/java/com/back/shared/post/event/PostCommentCreatedEvent.java b/src/main/java/com/back/shared/post/event/PostCommentCreatedEvent.java new file mode 100644 index 000000000..0be00713c --- /dev/null +++ b/src/main/java/com/back/shared/post/event/PostCommentCreatedEvent.java @@ -0,0 +1,11 @@ +package com.back.shared.post.event; + +import com.back.shared.dto.PostCommentDto; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public class PostCommentCreatedEvent { + private final PostCommentDto postComment; +} diff --git a/src/main/java/com/back/shared/post/event/PostCreatedEvent.java b/src/main/java/com/back/shared/post/event/PostCreatedEvent.java new file mode 100644 index 000000000..41be3214b --- /dev/null +++ b/src/main/java/com/back/shared/post/event/PostCreatedEvent.java @@ -0,0 +1,11 @@ +package com.back.shared.post.event; + +import com.back.shared.dto.PostDto; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class PostCreatedEvent { + private PostDto post; +} diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml new file mode 100644 index 000000000..31dea49eb --- /dev/null +++ b/src/main/resources/application-dev.yml @@ -0,0 +1,6 @@ +spring: + datasource: + url: jdbc:h2:./db_dev;MODE=MySQL + username: sa + password: + driver-class-name: org.h2.Driver \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index 24229f339..000000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ -spring.application.name=back diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 000000000..a4cc8123e --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,30 @@ +server: + port: 8081 +spring: + application: + name: back + profiles: + active: dev + output: + ansi: + enabled: always + jackson: + serialization: + fail-on-empty-beans: false + jpa: + hibernate: + ddl-auto: update + open-in-view: false + show-sql: true + properties: + hibernate: + format_sql: true + highlight_sql: true + use_sql_comments: true + default_batch_fetch_size: 100 +logging: + level: + com.back: DEBUG + org.hibernate.orm.jdbc.bind: TRACE + org.hibernate.orm.jdbc.extract: TRACE + org.springframework.transaction.interceptor: TRACE \ No newline at end of file