LostCatBox

SpringProject-Board-CH01

Word count: 1.9kReading time: 11 min
2022/12/24 7 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설정
  4. 4. 게시물 목록 및 작성 페이지 만들기
  5. 5. 추가적 질문사항에 대한 답변
  6. 6. 새로 배운부분
  7. 7. 추가적으로 학습할것