LostCatBox

SpringProject-Board-CH01

Word count: 1.9kReading time: 11 min
2022/12/24 Share

Spring 게시판 프로젝트 1편(게시판 생성)

Created Time: July 6, 2022 2:44 PM
Last Edited Time: August 3, 2022 11:07 AM
References: https://kyuhyuk.kr/article/spring-boot/2020/07/19/Spring-Boot-JPA-MySQL-Board-Write-Post
Tags: Java, Spring, Computer

기본 구조

스크린샷 2022-07-06 오후 3.18.30.png

DB설정

Mysql 선택한 이유

  • 생산성: 다른 DB를 크게 고려하지 않아도 될거같은 도메인 이라고 생각하여
    지금 만들려고 하는것은 제목, 내용이 담긴 post이기 때문이다
  • 대체적으로 post와 관련된 데이터들은 비정형 케이스를 찾기 힘들었음
  • 첫 프로젝트라서 가장 편한 것을 골랐다
  • post같은 경우에는 이후에 유저나 다른 관계매핑에 따라 관리되어야 할 가능성이있고, 그렇게때문에 연관관계 매핑이 간편한 mysql이 적합하다고 생각함

구현

https://kyuhyuk.kr/article/spring-boot/2020/07/19/Spring-Boot-JPA-MySQL-Board-Write-Post

1
2
3
4
5
6
7
mysql> CREATE BDATABASE POSTING_DB
-> DEFAULT CHARACTER SET UTF8;

mysql> create table POSTING.POST(
-> number int not null auto_increment primary key,
-> title varchar(200) not null,
-> content text not null);
  • db생성 및 유저 생성
    • 여기에서 나는 example대신 post_db로 하였다.
1
2
3
4
5
mysql -u root -p

mysql> create database example DEFAULT CHARACTER SET UTF8; -- example라는 데이터베이스를 생성합니다.
mysql> create user 'user'@'%' identified by 'UserPassword'; -- user라는 사용자를 생성합니다.
mysql> grant all on example.* to 'user'@'%'; -- user 사용자에게 example 데이터베이스의 모든 권한을 줍니다.

Spring 내 설정

  • build.grable 의 dependencies에 세팅 추가
1
2
3
4
5
6
7
8
9
10
11
12
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'mysql:mysql-connector-java'
annotationProcessor 'org.projectlombok:lombok'
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
runtimeOnly 'org.webjars:bootstrap:4.5.0'
}
  • resources의 application.properties
1
2
3
4
spring.jpa.hibernate.ddl-auto=update //JAVA의 Entity를 참고하여, SpringBoot실행시점에 자동으로 필요한 데이터베이스의 테이블 설정을 자동으로함 update(변경된 schema를 적용)
spring.datasource.url=jdbc:mysql://localhost:3306/example?serverTimezone=Asia/Seoul&characterEncoding=UTF-8
spring.datasource.username=user
spring.datasource.password=[비번]
  • spring.jpa.hibernate.ddl-auto : JAVA의 Entity를 참고하여, Spring Boot 실행 시점에 자동으로 필요한 데이터베이스의 테이블 설정을 자동으로 해줍니다.
    • none : 아무것도 실행하지 않습니다.
    • create : SessionFactory 시작 시점에 Drop을 실행하고 Create를 실행합니다.
    • create-drop : SessionFactory 시작 시점에 Drop 후 Create를 실행하며, SessionFactory 종료 시 Drop 합니다.
    • update : 변경된 Schema를 적용합니다. (데이터는 유지됩니다)
    • validate : update처럼 Object를 검사하지만, Schema는 아무것도 건들지 않습니다. 변경된 Schema 가 존재하면 변경사항을 출력하고 서버를 종료합니다.
  • spring.datasource.url : 데이터베이스의 URL입니다. 위의 URL은 example 데이터베이스를 입력했습니다 .
    • serverTimezone : 데이터베이스 서버의 시간을 ‘Asia/Seoul’로 설정합니다.
    • characterEncoding : 인코딩 방식을 ‘UTF-8’로 설정합니다.
  • spring.datasource.username : 데이터베이스에 접근할 사용자명 입니다.
  • spring.datasource.password : 데이터베이스에 접근할 사용자의 비밀번호 입니다.

게시물 목록 및 작성 페이지 만들기

  • 이하 이전에 만들었던 html들을 활용하기 때문에 크게 건드리지 않았다

스프링 구조

Untitled

  • 큰 구조와 동일하게 만들것이다
  • 글을 생성 및 내용변경는 Post방식으로 요청을 받아서, Service에서 처리되도록 할것이다.

DTO(Data Tranfer Object)를 통하여 Controller와 Service 사이의 데이터를 주고받을 것이다

Entity 구현하기

  • Entity는 DB table과 매핑되는 객체이다
  • domain/entity 에 위치
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// entity는 DB 테이블과 매핑되는 객체다
@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EntityListeners(AuditingEntityListener.class)// JPA에게 해당 Entity는 Auditing 기능을 사용함을 알립니다.
public class PostingEntity {
@Id
@GeneratedValue
private Long id;

@Column(length =10,nullable = false)
private String postName;

@Column(columnDefinition ="TEXT", nullable = false)
private String content;

@CreatedDate
@Column(updatable = false)
private LocalDateTime createdDate;

@LastModifiedDate
private LocalDateTime modifiedDate;

@Builder //매개변수를 builder 패턴으로 편리성,유연함을 줌
public PostingEntity(Long id, String postName, String content){
this.id = id;
this.postName = postName;
this.content = content;
}

}
  • JPA Auditing 기능을 사용하기 위해 main 클래스(PostingPracticeApplication)에 @EnableJpaAuditing
    어노테이션을 붙여줍니다.

Repository 구현

  • Repositroy는 데이터 조작을 담당하며, JpaRepository를 상속받는다.
  • JpaRepository의 값은 매핑할 Entity와 id의 타입이다.
  • JpaRepository를 상속받으면, 원래 손수 구현해야되는 save(), findById()등등이 이미 구현되어있다
  • domain/repository 패키지안에 DBPostingRepository 인터페이스를 만들었다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public interface DBPostingRepository extends JpaRepository<PostingEntity, Long> {
}
// 이친구가 옵셔널로 데이터를 주는 경우에 대해서.
// 스프링을 사용할때 왜 이렇게 c -> s -> r , r -> s -> c 구조를 유지하는가?
// 결국은 레이어에대한 책임과 역할 때문이다.
// 무슨말이냐? 일반적인 요청에 대한 흐름이 validation(검증) -> business(로직) -> cascade(영속화)
// 이 기본적인 생각을 머리에 품고,
// 근데 이게 왜 좋음? 왜 저렇게 해야 됨?
// 그냥 옵셔널 쭉 가지고 가다가 마지막에 검사하면 안됨?
// -> 안돼. 왜 와이, 그러면 중간 비즈니스가 반드시 모든 경우에 대해서 생각해줘야 돼.

// 다시 원제로 돌아와서,
// 이 인터페이스는 왜 옵셔널로 줄까? -> 있으면 데이터를 뽑아 주겠는데 그걸 확신을 못하니까
// 슈뢰딩거의 데이터다. (까보기전엔 몰?루)
// 인터페이스에서 이데이터에 접근할때는 강제로 get() or isPresent 확인을 해서 쓰도록 강제하고 있다.

// 그렇다면 위 생각과 접목시켜서 응용하면
// 1. repository -> service -> controller 로 갈때, 어느 부분에서 이 데이터가 있음을 검증하는 것이 옳을까?
// 2. 우리는 데이터가 있고 없을 때, 정책적으로 어떻게 결정할 것인가?

DTO 구현

  • Controller와 Service 사이에서 데이터를 주고받는 DTO(Data Transfer Object)를 구현한다.
  • DTO는 전달체일뿐(+직렬화함수 가짐), 로직을 포함해서는 안된다.
    반드시 로직은 Service에서만!
  • domain에 dto패키지 만들고 PostingDto 생성
  • 아래 toEntity()는 DTO에서 필요한 부분을 빌더 패턴을 통해 Entity로 만드는 일을 한다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//Controller와 Service 사이에서 데이터를 주고받는 DTO를 구현한다
@Getter
@Setter
@ToString
@NoArgsConstructor
public class PostingDto {
private Long id;
private String postName;
private String content;
private LocalDateTime createdDate;
private LocalDateTime modifiedDate;

public PostingEntity toEntity(){
PostingEntity posting = PostingEntity.builder()
.id(id)
.postName(postName)
.content(content)
.build();
return posting;
}

@Builder
public PostingDto(Long id, String postName, String content, LocalDateTime createdDate, LocalDateTime modifiedDate){
this.id = id;
this.postName = postName;
this.content = content;
this.createdDate = createdDate;
this.modifiedDate = modifiedDate;
}
}

DTO가 필요한 이유

  • 외부와 통신하는 프로그램에게 있어 호출은 큰 비용이며, 이를 줄이고 더욱 효율적으로 값을 전달할 필요가 있다. → 이를 위해 데이터를 모아 한번에 전달하는 방법이 고안되었다.
  • 데이터를 전송하기 위한 직렬화 메커니즘을 캡슐화하는 것! →본래는 controller layer에서 매번 해야하지만, DTO는 이 로직을 코드에서 제외하고(중복 제거), 원하는 경우 직렬화를 변경할수있는 명확한 지점을 제공한다.

Service 구현

  • 위에 만들어준 Repository를 사용하여 Service를 구현한다.
  • add, edit, delete 모두 구현
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@Service
public class PostingService {
private DBPostingRepository dbPostingRepository;

public PostingService(DBPostingRepository dbPostingRepository){
this.dbPostingRepository = dbPostingRepository;
}

@Transactional
public Long savePost(PostingDto postingDto){
return dbPostingRepository.save(postingDto.toEntity()).getId();
}
@Transactional
public Long editPost(PostingDto postingDto) {return dbPostingRepository.save(postingDto.toEntity()).getId();
}
@Transactional
public void deletePost(PostingDto postingDto) {
dbPostingRepository.delete(postingDto.toEntity());
}
@Transactional
public List<PostingDto> getPostList() {
List<PostingEntity> postingEntityList = dbPostingRepository.findAll();
List<PostingDto> postingDtoList = new ArrayList<>();

for(PostingEntity posting : postingEntityList) {
PostingDto postingDto = PostingDto.builder()
.id(posting.getId())
.postName(posting.getPostName())
.content(posting.getContent())
.createdDate(posting.getCreatedDate())
.build();
postingDtoList.add(postingDto);
}
return postingDtoList;
}
@Transactional
public PostingDto getPost(Long id){
Optional<PostingEntity> posting = dbPostingRepository.findById(id);
PostingDto postingDto = PostingDto.builder()
.id(posting.get().getId())
.postName(posting.get().getPostName())
.content(posting.get().getContent())
.createdDate(posting.get().getCreatedDate())
.build();
return postingDto;
}
}

Controller

  • add, edit, delete 모두 구현
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
@Controller
@RequestMapping("basic/posting")
public class PostingController {
private PostingService postingService;

public PostingController(PostingService postingService){
this.postingService = postingService;
}
@GetMapping
public String get(Model model){
List<PostingDto> postingDtoList = postingService.getPostList();
model.addAttribute("postings", postingDtoList);
return "basic/items";
}
@GetMapping("/{id}")
public String getpost(@PathVariable Long id, Model model){
PostingDto postingDto = postingService.getPost(id);
model.addAttribute("posting", postingDto);
return "basic/item";
}
@GetMapping("/{id}/edit")
public String getEditForm(@PathVariable Long id, Model model){
PostingDto postingDto = postingService.getPost(id);
model.addAttribute("posting", postingDto);
return "basic/editForm";
}
@PostMapping("/{id}/edit")
public String edit(@PathVariable Long id, @ModelAttribute PostingDto postingDto){
postingService.editPost(postingDto);
return "redirect:/basic/posting";
}
@GetMapping("/{id}/delete")
public String delete(@PathVariable Long id){
PostingDto postingDto = postingService.getPost(id);
postingService.deletePost(postingDto);
return "redirect:/basic/posting";
}
@GetMapping("/add")
public String post() {
return "basic/addForm";
}
@PostMapping("/add")
public String write(PostingDto postingDto){
postingService.savePost(postingDto);
return "redirect:/basic/posting";
}

}

추가적 질문사항에 대한 답변

왜 DTO를 사용해야하나?

  • DB와의 통신에 대해서는 비용이 많이 발생하는데, 이를 최소화하기위해 데이터 모아 한번에 전송하기위해 DTO를 사용한다.
  • DB에 전달하기 위해선 직렬화 과정이 필요한데, DTO에 직렬화에 관한 부분을 정의하므로써, 코드의 중복제거 및 책임을 강화할수있다.

추가

  • DAO, DTO, VO
    • DAO: Data Access Object 로 DB의 data에 접근하기 위한 객체다. DataBase접근 하기 위한 로직& 비즈니스 로직을 분리하기 위해 사용
      Service와 DB 연결하는 고리
    • DTO: Data Transfer Object 는 계층간 데이터 교환을 하기 위해 사용하는 객체>> 로직가지지 않고 순수한 데이터 객체(getter&setter만 가진 클래스)이다
      DB데이터를 Service와 Controller 사이에서 연결하는 고리
    • VO: 값 오브젝트로써 값을 위해 쓰인다. read-Only특징(DTO와 유사하나, setter가 없으므로 값 변화못함)

@builder의 장점

  • 점층적 생성자의 단점(매개변수 수에 따라 다른 생성자 정의, 사용자입장에서는 어떤것이 들어갓는지모름) 해결
  • @builder를 달면, 클래스 내부에 각 요소에 대한 setXXX가 생성되며 return 값으로 this를하여 Builder객체를 완성후 build()를 호출할때 최종적으로 원하는 객체를 반환해준다.(https://esoongan.tistory.com/82)

전체 구조에 관한 질문

  • 왜? 이런 구조를 가졋을까?
  • 아래구조

Untitled

  • 각 핵심 기능(역할)을 충실히 하고있다고 생각하면된다.
  • Dispatcher가 FrontController로의 역할로 Handler로 등록된 controller 찾고 해당되는 adapter를 찾아 handle()를 호출하여, 우리가 정의한 controller가 실행된다>> 그후 ModelAndView객체를 최종젹으로 Dispatcher가 받아 이를 View Resolver를 통해 논리 경로를 물리경로를 가진 view를 반환하여, view.render(각종인자)를 호출하여 최종적으로 view가 응답을 하게된다.
  • 알맞는 Adapter는 controller의 반환값에 따라서 결국엔 ModelAndView로 값을 변경후 반환하므로 Dispatcher입장에서 코드를 변경할 필요를 없게 만들어준다
  • Controller는 필요한 Service를 호출하여, 결과를 Model에 담고, view name반환
    이때는 DTO로 Service와 데이터 소통
  • Service는 핵심 비즈니스 로직을 가지고,Repository와 소통 이때는 Entity와 DTO간의 전환
  • Repository는 실제 DB와 Entity로 소통
  • 결국에는 Entity로 DB를 가져오고(자동) JPA가 제공하는 툴로 service나 controller에서는 DTO를 활용하고 service와 DB사이에서는 Entity를 활용해야함

추측

  • Entity로 JpaRepository를 연결했으므로, Entity는 하나의 테이블이라고 생각하면될것이다???
  • 도메인이란???

새로 배운부분

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Transactional
public PostingDto getPost(Long id){
Optional<PostingEntity> posting = dbPostingRepository.findById(id);

if (posting.isPresent) {

// 있으면 리턴한다.
return posting.get().toDto();
}
else {

// 없으면 다른 로직을 태운다.
TODO(do something else)
1. 그냥 빈 데이터를 보여준다 (디폴트 에러 객체를 내보낸다 or 다른 우회 로직)
=> 보안프로그램을설치하는지 체크합니다. -> 없어 -> 보안프로그램 설치 페이지로 -> 안내

2. 익셉션을 발생시킨다.
=> 없는 계좌번호로 송금시도 -> 없어 -> 에러
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// 다음 과제
// 사용자가 있음, 사용자는 회원가입으로 가입할 수 있고(시큐리티 적용같은건 자유, 그냥 아무렇게나 구현)
// 사용자는 글을 작성/수정/삭제 할 수 있음. 사용자는 자신의 글만 수정/삭제 할 수 있음.
// 사용자는 다른 사람의 글은 볼 수만 있음
// 각 글은 어떤사용자가 작성했는지 확인할 수 있음.

// 사용자는 다른 사람의 글에 댓글을 달 수 있음.
// 댓글은 댓글 작성자만 삭제할 수 있음.
// 원 글이 삭제되면 댓글도 같이 삭제됨.
// 대댓글은 달 수 없음.
// 댓글은 수정이 불가능함.

// 사용자는 자신이 작성한 글과 댓글을 상시 확인할 수 있음.

추가적으로 학습할것

스프링 계층(https://devlog-wjdrbs96.tistory.com/209)

  • 아예 포트폴리오 만드신분 여기

[https://dev-coco.tistory.com/111?category=1032063](

CATALOG
  1. 1. Spring 게시판 프로젝트 1편(게시판 생성)
  2. 2. 기본 구조
  3. 3. DB설정
    1. 3.1. Mysql 선택한 이유
    2. 3.2. 구현
    3. 3.3. Spring 내 설정
  4. 4. 게시물 목록 및 작성 페이지 만들기
    1. 4.1. 스프링 구조
    2. 4.2. Entity 구현하기
    3. 4.3. Repository 구현
    4. 4.4. DTO 구현
      1. 4.4.1. DTO가 필요한 이유
    5. 4.5. Service 구현
    6. 4.6. Controller
  5. 5. 추가적 질문사항에 대한 답변
    1. 5.1. 왜 DTO를 사용해야하나?
      1. 5.1.1. 추가
    2. 5.2. @builder의 장점
    3. 5.3. 전체 구조에 관한 질문
    4. 5.4. 추측
  6. 6. 새로 배운부분
  7. 7. 추가적으로 학습할것