Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

runtimeOnly 'com.h2database:h2'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'

implementation 'org.springframework.boot:spring-boot-starter-validation'
}

tasks.named('test') {
Expand Down
17 changes: 17 additions & 0 deletions src/main/java/com/example/devSns/DevSnsApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import com.example.devSns.domain.post.Post;
import com.example.devSns.domain.post.PostRepository;

@SpringBootApplication
public class DevSnsApplication {
Expand All @@ -10,4 +14,17 @@ public static void main(String[] args) {
SpringApplication.run(DevSnsApplication.class, args);
}

// 실행 시 더미데이터 자동 삽입
@Bean
CommandLineRunner init(PostRepository repo) {
return args -> {
if (repo.count() == 0) {
repo.save(Post.builder()
.content("hello h2")
.username("hong")
.likes(0)
.build());
}
};
}
Comment on lines +18 to +29
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

h2 사용하실 때 더미 데이터 필요하시면 data.sql 파일에 작성하시고 application.properties에 더미데이터 생성 경로 적어두는 방식으로도 구성할 수 있습니다! 이러면 코드에 데이터 초기화 로직이 안 들어가도 돼서 깔끔하게 구성 가능합니다.

# data.sql
INSERT INTO post (content, username, likes) VALUES ('hello h2', 'hong', 0);

}
8 changes: 8 additions & 0 deletions src/main/java/com/example/devSns/common/config/JpaConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.example.devSns.common.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@Configuration
@EnableJpaAuditing
public class JpaConfig {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.example.devSns.common.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<?> handleIllegalArgument(IllegalArgumentException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage());
}

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<?> handleValidation(MethodArgumentNotValidException e) {
String msg = e.getBindingResult().getFieldErrors().stream()
.map(err -> err.getField() + ": " + err.getDefaultMessage())
.findFirst().orElse("Validation error");
return ResponseEntity.badRequest().body(msg);
Comment on lines +17 to +22
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

예외 처리까지 구성해주셨네요! 그런데 현재 방식에서는 검증 실패 내역 중 첫 번째만 반환하는 구성이라 추후 joining등을 사용해서 검증 실패 내용을 전부 알려주는 형식이 더 좋을 것 같습니다!

}
}
27 changes: 27 additions & 0 deletions src/main/java/com/example/devSns/domain/post/Post.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// com.example.devSns.domain.post.Post
package com.example.devSns.domain.post;

import jakarta.persistence.*;
import lombok.*;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.time.LocalDateTime;

@Entity @Table(name="posts")
@EntityListeners(AuditingEntityListener.class)
@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setter를 최대한 지양하고 update 형식으로 객체에게 일을 시키는 관점으로 리팩토링 해 보시면 더 좋을 것 같습니다!

public class Post {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable=false, length=1000)
private String content;
@Column(nullable=false)
private String username;
@Column(nullable=false)
private Integer likes;
@CreatedDate @Column(updatable=false)
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// com.example.devSns.domain.post.PostRepository
package com.example.devSns.domain.post;
import org.springframework.data.jpa.repository.JpaRepository;
public interface PostRepository extends JpaRepository<Post, Long> {}
59 changes: 59 additions & 0 deletions src/main/java/com/example/devSns/service/post/PostService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.example.devSns.service.post;

import com.example.devSns.domain.post.Post;
import com.example.devSns.domain.post.PostRepository;
import com.example.devSns.web.post.dto.PostDtos.CreateReq;
import com.example.devSns.web.post.dto.PostDtos.UpdateReq;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
@RequiredArgsConstructor
public class PostService {

private final PostRepository postRepository;

@Transactional
public Post create(CreateReq req) {
Post p = Post.builder()
.content(req.content)
.username(req.username)
.likes(0)
.build();
return postRepository.save(p);
}

@Transactional(readOnly = true)
public Post get(Long id) {
return postRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Post not found: " + id));
}
Comment on lines +19 to +33
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

읽기 전용 시 readOnly=true 잘 적용해주셨네요!


@Transactional(readOnly = true)
public List<Post> list() {
return postRepository.findAll();
}

@Transactional
public Post update(Long id, UpdateReq req) {
Post p = get(id);
p.setContent(req.content);
return p;
}

@Transactional
public void delete(Long id) {
Post p = get(id);
postRepository.delete(p);
}

@Transactional
public Post like(Long id) {
Post p = get(id);
p.setLikes(p.getLikes() + 1);
return p;
}
}
68 changes: 68 additions & 0 deletions src/main/java/com/example/devSns/web/post/PostController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.example.devSns.web.post;

import com.example.devSns.domain.post.Post;
import com.example.devSns.service.post.PostService;
import com.example.devSns.web.post.dto.PostDtos.CreateReq;
import com.example.devSns.web.post.dto.PostDtos.UpdateReq;
import com.example.devSns.web.post.dto.PostDtos.Res;

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/posts")
public class PostController {

private final PostService postService;

// Create
@PostMapping
public Res create(@Valid @RequestBody CreateReq req) {
return toRes(postService.create(req));
}

// Read one
@GetMapping("/{id}")
public Res get(@PathVariable Long id) {
return toRes(postService.get(id));
}

// Read all
@GetMapping
Comment on lines +23 to +35
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 Dto를 바로 반환하는 구조네요! 그런데 이 경우 서버에서 항상 기본값인 200 상태코드만 반환하게 됩니다. ResponseEntity를 적용해보시면 좋을 것 같습니다.

public List<Res> list() {
return postService.list().stream().map(this::toRes).toList();
}

// Update
@PutMapping("/{id}")
public Res update(@PathVariable Long id, @Valid @RequestBody UpdateReq req) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Res라는 변수명은 나중에 다른 ResponseDto가 생기면 헷갈릴 수도 있을 것 같습니다! 의도가 드러나도록 조금 더 구체적으로 명명하면 더 좋을 것 같습니다

return toRes(postService.update(id, req));
}

// Delete
@DeleteMapping("/{id}")
public void delete(@PathVariable Long id) {
postService.delete(id);
}

// Like
@PostMapping("/{id}/like")
public Res like(@PathVariable Long id) {
return toRes(postService.like(id));
}

private Res toRes(Post p) {
Res r = new Res();
r.id = p.getId();
r.content = p.getContent();
r.username = p.getUsername();
r.likes = p.getLikes();
r.createdAt = p.getCreatedAt();
r.updatedAt = p.getUpdatedAt();
return r;
}
Comment on lines +58 to +67
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이것도 조금 더 객체지향적으로 본다면 Res에 Post를 인자로 받는 생성자를 추가하고 Res 생성은 Res에만 위임하면 Controller 계층의 코드를 더 간결하게 구성할 수 있을 것 같습니다.

}
29 changes: 29 additions & 0 deletions src/main/java/com/example/devSns/web/post/dto/PostDtos.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.example.devSns.web.post.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import java.time.LocalDateTime;

public class PostDtos {

public static class CreateReq {
@NotBlank @Size(max = 1000)
public String content;
@NotBlank
public String username;
}

public static class UpdateReq {
@NotBlank @Size(max = 1000)
public String content;
}

public static class Res {
public Long id;
public String content;
public String username;
public Integer likes;
public LocalDateTime createdAt;
public LocalDateTime updatedAt;
}
}
12 changes: 11 additions & 1 deletion src/main/resources/application.properties
Original file line number Diff line number Diff line change
@@ -1 +1,11 @@
spring.application.name=devSns
spring.datasource.url=jdbc:h2:mem:devsns;MODE=MySQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=

spring.h2.console.enabled=true
spring.h2.console.path=/h2-console

spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true