[SPRING_입문]/Code Review

[Localhost] Timeline Service 구축 (Server / Client)

Code_Otaku 2022. 6. 23. 16:56

[Timeline Service Client]

academy3746/TimeLine_Service: 타임라인 어플리케이션을 A-Z 까지 혼자 만들어보자! (github.com)

 

GitHub - academy3746/TimeLine_Service: 타임라인 어플리케이션을 A-Z 까지 혼자 만들어보자!

타임라인 어플리케이션을 A-Z 까지 혼자 만들어보자! Contribute to academy3746/TimeLine_Service development by creating an account on GitHub.

github.com

 

벼르고, 또 벼르고 있다가 마침내 포스팅을 올린다.

3주차 Java Spring 수업시간에는 Timeline Service를 직접 만들어 보았다.

그리고 필자는 분명 서버단부터 클라이언트까지 전체 코드를 해부하여 부검하듯이 훑어보겠다고 했다.

 

비도 오고, 어깨도 쑤시고 왠지 깝깝한 마음부터 들지만..

이런 수고로움을 감당해야 뭐 하나라도 내 것으로 건져 올리지 않겠는가?

 

Front / Back 모두 리뷰 해볼테지만 필자는 서버 이슈쪽에 더 집중해서 올릴 생각이다.

프론트 (HTML / CSS / JavaScript / jQuery)까지 폭 넓게 다루기에는 아직 필자의 지식이 부족하기 때문이다.

그 점은 양해해주시길 바란다..

 

그럼 여기서 우선 임시저장을 하고..

본격적으로 코드리뷰를 시작해보도록 하겠다.

아쎄이.. 기상..!

 


 

1. Application [Server] Scope

어플리케이션을 작동시키는 서버단은 다음과 같이 크게 세 파트로 구성되어 있다.

 

  • Domain
  • Service
  • Controller

 

각각 수행하는 역활이 구분되어 있고, 결코 내용도 적지 않지만 인내심을 가지고 하나씩 뜯어보자.

핵심 기능을 수행하는 라인 몇줄만 파악하면 아무리 복잡한 코드라도 겁 먹을 필요 하나도 없다.

아직 먼 훗날의 이야기지만 우리는 웹 / 앱에서 한 단계 더 뛰어넘어야 밥 벌이를 할 수 있으니 말이다.

 

1-1) Domain

  • Memo.java [Java Class]
  • TimeStamped.java [Java Class]
  • MemoRequestDto.java [Java Class]
  • MemoRepository.java [Java Interface]

 

서버 도메인 역시 보시는 바와 같이 4개의 자바 클래스에서 각자의 역활을 수행한다.

 

우선 도메인 패키지 전체를 뭉뜽그려서 보면 마치 하나의 커다란 내장객체 (Container)와도 같다.

내장객체를 이번 장에서 개념부터 다루기에는 다소 무리가 있다.

 

그냥 서버와 클라이언트 간 이루어지는 요청 (Request)응답 (Response)

보이지 않는 수면 아래에서 기능적으로 수행하는 녀석이라고만 이해하고 넘어가자.

 

좀 더 쉽게 예를 들어볼까?

카운터에서 빅맥세트 주문이 들어왔다면 그때부터 주방은 겁나게 분주해진다.

누군가는 빵을 삶아야 할 것이며..

다른 누군가는 패티를 구워야 할 것이다.

또 다른 한 쪽에서는 감자튀김을 튀겨야겠지?

 

정적 (Static) 혹은 동적 (Dynamic) 웹 어플리케이션도 똑같다.

이 놈은 도대체 뭐에 써먹는 물건인고? 싶은 객체 (Object)들도 다 주어진 역할이 있는 셈이다.

 

1-1-1) Memo.java

첫 번째 구성요소인 Memo 클래스부터 살펴보도록 하자.

이 녀석은 마치.. 패티와 같다.

햄버거에서 패티가 빠지면 무슨 맛으로 먹겠는가?

그러니까 패티는 버거의 기초 베이스가 되는!

절대, 절대로 빠져서는 안되는!

가장 핵심적인 재료인 것이나 마찬가지이다.

 

이처럼 Memo 클래스 (패티...)는 이번 프로젝트에 사용될 모든 핵심 변수 (Variable)들을 저장하고 있다.

@Id
// CREATE TABLE, Primary Key
@GeneratedValue(strategy = GenerationType.AUTO)
// id ++;
private Long id;
// Table No.

@Column(nullable = false)
// This Column must have value
private String username;

@Column(nullable = false)
// This Column must have value
private String contents;

뭔가 복잡해보이지만 이번 프로젝트에서 사용하는 변수는 딱 세 개이다.

그 중에서도 가장 필요한 녀석들도 두 개로 추려진다.

 

우선 id는 Long타입 변수로서, DB 테이블에서 각 항목을 구분짓기 위해 만든 Table Number와도 같은 녀석이다.

username은 말 그대로 사용자 이름!

contents는 포스트 박스 안에 들어갈 내용물이겠지 뭐..

ID USER_NAME CONTENTS
1 Admin Hello!
2 James Nice to meet you.
3 Robert How are you?
4 John I'm fine, thank you.
5 Kate What about you?

짜잔~!

유저에게 직접적으로 노출되는 부분이 있고, 그렇지 않은 부분도 있겠지만..

서버 DB 상에서는 대체로 저런식으로 테이블이 생성 (CREATE)될 것이다.

 

'패티의 재료는 다음과 같읆..'

식으로 자바에게 알려주기 위한 목적이라고 할 수 있겠다.

 

'@'는 어노테이션 (Annotation)이라고 한다.

Lombok 등의 라이브러리를 호출하기 위해 사용한다.

 

우선 @Id는 서버에서 자동으로 테이블을 생성해주기 위해 자바에게 알려주는 역활을 담당하고 있다.

참고로 Primary Key이다.

 

@GeneratedValue(strategy = GenerationType.AUTO)는 테이블 번호를 자동으로 생성해줘! 라는 뜻

 

username과 contents 변수 윗단에 각각 자리잡은 @Column(nullable = false) 는 그러니까.. 

해당 컬럼 (행)에서 null이 있어서는 안됨! 정도가 되겠다.

여기서 null은 값이 존재하지 않는다! 라는 뜻이지만..

 

사실 자바에서는 null을 참조형 데이터타입으로 취급한다.

참조 (Reference)는 매우매우 중요한 내용이기 때문에

자바 기본 개념을 정리할 때 따로 시간을 할애하도록 하겠다.

 

package com.sparta.timeline.domain;

import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.*;

 

 

이 녀석은 클래스의 최상단에 위치해있다.

이클립스 개발환경에서는 저 놈들을 하나, 하나 임포트 해줘야 하는 번거로움이 있다.

말인즉슨 내가 쓰고자하는 객체들의 기능을 잘 모른다면 단 한 개의 라이브러리도 제대로 쓸 수가 없다.

하나씩 차근차근 공부하지 않으면 안되겠지?

 

반면에 Spring Boot 환경에서는 Auto Import 기능을 제공하고 있기 때문에

시키지 않아도 자바에서 알아서 척척 적재적소에 임포트를 실행해준다.

그렇기 때문에 이것이 무엇에 쓰는 객체인지 모르고 넘어가기엔 딱인 것이다.

그러니까 변태같이 한 줄씩 해부해보자.

 

package com.sparta.timeline.domain;

어어.. 링크 타고 들어가지 말아라.

그냥 내가 만든 클래스에 어디에 위치해 있는지를 알려주는 경로 (Path) 정도로 이해해라.

 

import lombok.Getter;

내가 넣어준거 아니다..

똑똑한 스프링 선생께서 알아서 넣어준 놈이다.

각 변수 앞에 'private'이 붙었던거 기억하는가?

데이터의 유실을 막기 위해 접근을 제한한 것이다.

저런 식으로 접근을 제한하면 다음과 같은 현상이 발생한다.

 

public Long id;

public String username;

public String contents;

 

public은 어느 곳에서나 가져다가 쓸 수 있는 공중변소 같은 느낌인데..

이미 Memo 클래스에서 접근제한자를 걸어놨기 때문에 가져다가 쓸 수 없다.

굳이 쓰고 싶다면 memo.getId();

요런 식으로 써야 하는데 이것도 당장에 여의치가 않다.

따라서 Memo 클래스 자체적으로 @Getter를 선언해줘야 제한적으로 가져다가 쓸 수 있다.

마찬가지로 저 변수들을 자식 클래스에서 재정의 (Override) 하기 위해서는 @Setter를 선언해줘야 한다.

우선 이 프로젝트에서 Setter는 쓸 일이 없다.

그리고 오버라이드 역시 상속과 다형성 기능에서 빠져서는 안되는 개념이지만..

그.. 잘 아시잖습니까?

 

import lombok.NoArgsConstructor;

요놈은 또 뭐징???

 

public Memo(String username, String contents){
    this.username = username;
    this.contents = contents;
}

 

다음 코드블록을 보기 바란다.

뭔가 허전하지 않음?

처음부터 그렇게 느꼈다면 당신은 굉장히 감이 좋은 것이다.

자바의 입장에서 봤을 때 'public Memo(...) {...}' 의 정체가 참 모호하다..

 

이 놈이 클래스인지, 인터페이스인지, 아니면 추상 클래스인지.. 뭘로 구분할건데?

 

사실 Memo 클래스 안에 들어가있는 변수들을 끄집어 쓰기 위함인데

불친절한 자바에서는 그렇게 쓰면 안된다.

반드시 빈 공간에다가 'public (...) Memo(...) {}' 식으로 속이 텅! 비어있는 기본생성자를 따로 만들어줘야 한다.

 

그런데 귀찮잖아?

 

@Getter
// Auto Import using Lombok Lib
@NoArgsConstructor
// Public class (   ) Memo
@Entity
// Hey, Java! There is table on DB Server!

 

그렇다면 요런 식으로 어노테이션을 선언해주면 된다.

 

여기서 쓰고자하는 어노테이션은 @NoArgsConstructor 이다.

파라미터의 값이 비어있는 생성자를 만들어줄래? 라는 뜻

 

내친 김에 하나 남은 어노테이션도 여기서 정리해야겠다.

@Entity

어이 자바! 이 페이지에는 테이블이 있어! 정도로 이해하면 되겠다.

 

import javax.persistence.*;

사실 필자도 이놈은 구글링 해봤다.

Spring Boot에서 가장 강력한 기능인 JPA가 무엇의 줄인말인지 아는가?

Java Persistence API 라고 한다.

여기까지만 살펴봐도 JPA와 연관된 객체겠거니.. 하면서 느낌이 오지 않는가?

 

객체지향 언어인 자바를 더욱 객체지향적으로 만들어주는 녀석이라고 보면 된다.

퍼시스턴스 앞에 // 으로 주석처리를 해보싈?

신기하게도 DB에서 테이블 영역에 속하는 모든 어노테이션 기능들이 못쓰는 물건으로 변해버린다.

즉 테이블을 CRUD 하기 위해서 반드시 필요한 객체 정도로 이해하면 되지 않을까?

이 정도로밖에 설명하지 못해서 미안하다.. 필자도 아직 배워야 할 게 산더미인 사람이다.

 

public Memo(MemoRequestDto requestDto){
    this.username = requestDto.getUsername();
    this.contents = requestDto.getContents();
}

 

하마터면 까먹을 뻔했다.

Memo 클래스 리뷰를 다 한 줄 알고 한 템포 쉬어가자고 한건데 아직 조금 남아있었다.

그런데 어짜피 깊게 들어가지는 못한다.

 

이번 코드블럭을 한 번 살펴보자.

뭔가 위화감이 느껴지지 않은가?

Memo 클래스에서 사용 가능한 변수는 Long 타입 id하고, String 타입 username / contents 밖에 없었는데?

여기서 다룰법한 내용은 아니지만 저게 가능한 이유는..

메서드 오버로드 (Overload) 덕분이다.

 

도대체 그게 뭔데?

자바 기본개념에서 자세히 다루겠다.

당장은 짐을 이빠이 실은 화물차를 떠올려보기 바란다.

흔히 과적이라고 해서 단속대상에 들어가지?

 

메서드 오버로드도 이와 같다.

파라미터 명을 다르게 설정해 줄 수 있는 것이다.

즉 MemoRequestDto 클래스를 requestDto 라는 이름의 변수로 오버로드 해서 쓰겠다는 뜻이다.

당연히 MemoRequestDto는 미리 만들어놓은 클래스이다.

안그랬으면 또 빨간줄로 오류가 떴을걸?

 

당장 저 클래스를 깊게 다루지는 않겠다.

아니 어짜피 이따가 다뤄야 한다.

그냥 requestDto 라는 화물차에서 뭔가가 바리바리 내리고 있다는 것 정도만 개념을 잡아라.

 

public void update(MemoRequestDto requestDto){
    this.username = requestDto.getUsername();
    this.contents = requestDto.getContents();
}

 

하.. 이번에는 또 뭔데?

자자, 성질부터 내지 말고..

타임라인 서비스의 핵심 중추 기능에는 뭐가 있었지?

 

C.R.U.D!

해당 코드블럭은 바로 타임라인을 UPDATE 해주기 위해 별도로 선언한 메서드이다.

당연히 업데이트 기능을 수행하기 위한 클래스도 따로 만들어 놓았다.

 

void는 '비어있는' 이라는 상투적인 뜻이다.

Return을 받지 않겠다는 의도이다.

사실 @Getter를 선언하지 않는다면 저런 식으로 Getter 영역을 따로 만들어줘야 한다.

requestDto 내장객체에서 username과 contents를 변수를 따로 가져오겠다는 뜻이다.

그래야지 저 두 변수를 업데이트 해줄 수 있을거 아니야? 그치?

 

Memo.java 클래스의 코드리뷰는 진짜 여기서 마치겠다.

다음으로는 타임라인을 24시간 간격으로 갱신해주기 위한 TimeStamped.java 클래스를 파헤쳐보자.

 


1-1-2) TimeStamped.java

이전에 나갔던 숙제 기억하는가?

내 블로그를 꾸준히 들어오는 분들은 없을테니 기억날리가 없지..

잠깐만~

[week03_hw]

 

이런 내용의 과제였던 거 같다.

조회 시간은 당연히 Current Time을 말하는거지.

지금이 아니면 언제 조회하겠음?

글을 작성하는 현재 시각이 2022년 06월 23일 20시 15분이니까..

2022년 06월 22일 20시 15분부터 타임라인에 올라온 메시지들은 전부 조회하겠다는 뜻이다.

 

24시간 기준!

 

@Getter
// Time Date 에 각각 접근제한자를 생성해줫기 때문에 반드시 Get 으로 받아야 한다. 안그럼 컴파일 에러 뜸..
@MappedSuperclass
// Entity 가 자동으로 Column 을 인식
@EntityListeners(AuditingEntityListener.class)
// 타임라인 생성시간을 자동으로 Update

public abstract class TimeStamped {
    // Memo Class 의 Parent Class

    @CreatedDate
    private LocalDateTime createAt;
    // 타임라인 생성시간
    
    @LastModifiedDate
    private LocalDateTime modifiedAt;
    // 타임라인 수정시간

}

 

TimeStamped.java 클래스는 그 기능을 수행하기 위해 반드시 필요한 녀석이다.

나중에 설명하겠지만..

Repository.java 클래스에서 JPA 컬렉션을 이용하여 그 기능을 직접 구현해줄 것이기 때문이다.

거기에서 참조할 변수를 따로 저장해주는 클래스라고 보면 된다.

 

LocalDateTime은 타임라인 작성(POST) / 수정(PUT) / 삭제(DELETE) 시간을

실시간 (Real Time)으로 반영해주기 위한 메서드이다.

 

createAt는 타임라인 작성시간!

modifiedAt는 타임라인 수정시간!

하지만 이대로 끝내버리면 이도저도 아닌 타입이 되어버리.. 지는 않겠지만..

 

일처리를 더 확실하게 하기 위해 각각 어노테이션을 만들어주었다.

@CreatedDate

@LastModifiedDate

한 눈에 보기에도 직관적이지 않은가?

특히 저 @LastModifiedDate를 눈여겨 보기 바란다.

 

사실 그것보다 더 중요한게 코드블록 상단에 호출된 3개의 어노테이션이다.

 

@Getter
// Time Date 에 각각 접근제한자를 생성해줫기 때문에 반드시 Get 으로 받아야 한다. 안그럼 컴파일 에러 뜸..
@MappedSuperclass
// Entity 가 자동으로 Column 을 인식
@EntityListeners(AuditingEntityListener.class)
// 타임라인 생성시간을 자동으로 Update

 

중요하니까 따로 카피해온거다.

필자가 주석처리 한 부분도 그냥 넘어가지 않길 바란다.

리얼타임으로 반응하는 타임스탬프를 만들어 줄때는 저 세 어노테이션 중에 어느 하나라도 빠지면 동작하지 않는다.

 

@Getter는 코멘트에서처럼 접근제한자의 값을 얻어오기 위함이고..

 

@MappedSuperclass는 LocalDateTime 데이터를 DB 테이블에서 컬럼 값으로 INSERT 해주기 위한 녀석이다.

 

@EntityListeners(AuditingEntityListener.class)는 타임라인의 작성시간을 실시간으로 얻어오기 위한 녀석이다.

 

 

아! 정말로 정말로 중요한 부분을 빼먹은 거 같다.

잠깐 코드블록을 되짚어보자.

 

public abstract class TimeStamped {

... (생략) ...

}

 

요 부분이다.

 

TimeStamped.java는 추상 (Abstract) 클래스이다.

 즉! 완성되지 않은 클래스라는 것이다.

당연히 자식객체에서 상속받은 메서드를 오버라이딩 하여서 완성시켜야 한다.

한 마디로 TimeStamped.java 클래스는 객체와 메서드를 여기저기 뿌려주는 기능이 최우선인 것이다.

 

가볍게 훑고 넘어가지 말아라.

이 부분을 간과하면 디버깅 할 때 심각한 오류가 발생할 것이다.

필자가 장담한다.

 

TimeStamped.java 클래스에 대한 리뷰는 이정도면 충분한 것 같다.

이제는 MemoRequestDto.java 클래스로 넘어가보자! 

 


1-1-3) MemoRequestDto.java

 

@RequiredArgsConstructor
@Getter

public class MemoRequestDto {

    private final String username;
    private final String contents;

}

 

 

바로 코드블록을 살펴보도록 하자.

코드 자체는 별 내용이 없다.

Memo.java 클래스에서 private으로 선언한 username과 contents 변수를 각각 얻어오기 위해

@RequriedArgsConstructor을 선언하였을 뿐이다.

@NoArgsConstructor가 비어있는 생성자라면 @RequriedArgsConstructor는 필요한 생성자를 자동으로 Import 해온다.

 

동작 그만..

 

필자가 Bold 처리한 부분을 그냥 넘어간 거 아니겠지?

 

필요한 생성자라고 했다.

저 두 변수는 반드시 필요한 변수이다.

모든 클래스와 심지어 DB 서버에까지 값을 전달해 줘야할 녀셕들이다.

너 이거 가져다쓰지 않으면 죽어?

하면 뭐다?

IT's a final.. final.. final countdown!!!

 

... 동작 그만...

 

이번에도 Bold 처리해준 녀석이 있다.

 

바로 '전달 (Transfer)'!

 

이것이 MemoRequestDto.java 클래스의 A-Z 라고 할 수 있다.

Data Transfer Object!

바로 데이터를 전달해주기 위하여 따로 생성한 객체라는 것이다.

 

어디로?

DB 서버로..

 

본 프로젝트에서는 MySQL과 H2 Database 서버를 사용하였다.

 

뭐 더 설명이 필요한가? 여기까지만 알아도 패티 절반은 구운 셈이다.

조금만 더 힘을 내서 MemoRepository.java 클래스로 넘어가보도록 하자.

 


1-1-4) MemoRepository.java

 

이번에도 코드블록 먼저...

 

import java.time.LocalDateTime;
import java.util.List;

public interface MemoRepository extends JpaRepository<Memo, Long> {

    List<Memo> findAllByModifiedAtBetweenOrderByModifiedAtDesc(LocalDateTime start, LocalDateTime end);
    // findAll By ModifiedAt Between Order By ModifiedAt Desc
    // -> SELECT * FROM TABLE ORDER BY ModifiedAt DESC;
    // BETWEEN: Time between yesterday & today

}

 

주석이 주렁주렁 달려 있으면 오히려 더 헷갈리니까 중요한 부분만 발췌해서 따로 살펴보도록 하자.

 

public interface MemoRepository extends JpaRepository<Memo, Long> {

    List<Memo> findAllByModifiedAtBetweenOrderByModifiedAtDesc(LocalDateTime start, LocalDateTime end);
    
}

 

뭐가 좀 보이나?

첫번째 라인부터 차근차근 해부 해보도록 하자.

 

public interface MemoRepository extends JpaRepository<Memo, Long>

 

우선 MemoRepository.java는 일반적인 클래스가 아니다.

추상 클래스와 마찬가지로 상속이 강제되는 인터페이스 (Interface)임을 주지시켜주고 있다.

JpaRepository<>를 상속 (extends)하고 있네?

이 경우에는 JpaRepository<>가 부모 (조상) 클래스인 것이다.

 

<>는 Generics라고 하는 기법이다.

List<Long, id> 처럼 변수값을 배열로 불러오는 형태 기억하는가?

이번에는 Memo.java 클래스에서 Long 타입 변수를 가져오겠다고 선언하고 있다.

 

세번째 라인으로 넘어가보자.

 

좀 전에 설명했던 것과 마찬가지로 Memo.java 클래스에 저장된 변수들을 배열로 불러오고 있다.

 

무엇보다 주목해야 할 부분은 바로 여기!

findAllByModifiedAtBetweenOrderByModifiedAtDesc()

알아보기 쉽게 띄어쓰기로 구분해주자.

find All By ModifiedAt Between OrderBy ModifiedAt Desc

 

TimeStamped.java 클래스에 분명히 modifiedAt 이라는 이름의 변수가 있었다.

바로 여기에 써먹으려고 했던 것이다.

findAllByModifiedAtBetweenOrderByModifiedAtDesc() 라는 이 기다란 메서드는 의외로 규칙이 있다.

필자가 멋대로 지어준 이름이 아니라는 것이다.

 

우선 findAllBy은 SQL문에서는 SELECT * FROM TABLE_NAME 정도로 표현할 수 있겠다.

테이블 내에 존재하는 데이터는 전부 다 조회 (GET) 하겠다는 뜻이다.

findAllBy 역시 같은 의미이다.

 

Between은 뭐 뻔한거 아닌가?

현재 조회하려는 시점으로부터 정확히 24시간 이내로 존재하는 데이터들만! 이라고 WHERE 절처럼 조건을 둔 것이다.

 

OrderBy는 SQL문과 마찬가지로 정렬기준을 정해준건데 그 기준이..

 

ModifiedAt DESC

그러니까 수정시간을 내림차순으로 정렬해달라는 뜻이다.

그럼 클라이언트 화면에서는 가장 최근에 올린 컨텐츠가 맨 위에 노출되겠지?

 

ModifiedAt 변수명만 제외하면 JPA에서 정해준 규칙대로 나열한 것이다.

요만큼의 수정도 있어서는 안된다.

프레임워크는 그러한 점에서 라이브러리와 다르게 규칙성과 강제성을 띄고 있는 것이다.

 

항상 다 쓰고 나면 가장 중요한 부분을 빠뜨리게 되더라..

코드 자체에만 집중한 나머지 MemoRepository.java가 뭐하는 녀석인지를 설명 안한 것 같다.

Git 저장소 (Repository)를 떠올려보면 쉽다.

한 마디로 레파지토리 클래스는 Memo.java의 데이터들을 우선 불러온 다음에..

그것들의 시간 값 (LocalDateTime)을 따로 저장하기 위해 만든 녀석이다.

start와 end로 각각 파라미터를 지정해줬다.

 


Domain 패키지에 대한 리뷰는 이정도면 충분한 것 같다.

이제 서버단의 절반은 온 셈이다.

세 줄 요약 같은건 없다.. 미안하다.

그냥 서버단에서 가장 분주하게 움직이는 녀석이라고 이해하면 쉬울 것이다.

다음으로 ServiceController를 마저 리뷰함으로서 서버단은 마칠까 한다.

그렇게 글이 늘어지지는 않을 것이다.

아마도..

 


1-2) Service

  • MemoService.java

 

Domain과 마찬가지로 Service 패키지 역시 필요한 파라미터를 담고 있는 하나의 커다란 내장객체이다.

천만 다행으로 하위 클래스도 딱 하나고 코드도 복잡하지 않다.

다만 그 역활만 확실하게 짚고 넘어가도록 하자.

거두절미하고 소스코드 먼저 확인할까?

 

@Service
@RequiredArgsConstructor

public class MemoService {

    private final MemoRepository memoRepository;

    @Transactional
    public Long update(Long id, MemoRequestDto requestDto){
        Memo memo = memoRepository.findById(id).orElseThrow(
                () -> new IllegalArgumentException("해당 아이디가 존재하지 않습니다!")
        );

        memo.update(requestDto);
        return memo.getId();
    }

}

코드블록은 이게 전부다.

눈여겨 볼 내용을 차근 차근 뜯어보도록 하자.

 

@Service

골뱅이가 붙는 이 녀석을 뭐라고 했지?

Annotation이라고 했다.

굳이 다시 환기시키는 이유는 Spring Boot에서 그만큼 중요하고 핵심적인 기능이기 때문이다.

 

요 놈의 역활은 단순하다.

"어이, 자바! 이 페이지는 서비스 기능을 담당할테니 그렇게 알고 있으라고!"

 정도로 이해하면 되겠다.

 

이해를 돕기 위해서 앞에 Domain 패키지를 주방으로 비유했다.

빵을 삶고, 패티를 굽고, 감튀를 튀기고, 음료를 준비하고.. 식으로 말이다.

Service.java 클래스는 빅맥세트의 포장을 담당하는 녀석 정도가 되겠다.

다음 소스코드를 살펴보자.

 

import com.sparta.timeline.domain.Memo;
import com.sparta.timeline.domain.MemoRepository;
import com.sparta.timeline.domain.MemoRequestDto;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import javax.transaction.Transactional;

 역시나 클래스 최상단에 자동으로 Import되는 객체들이다.

위에서부타 딱 3줄만 훑어보자.

Memo.java / MemoRepository.java / MemoRequestDto 클래스들을 모두 참조하고 있다.

소스코드의 내용 자체는 별 거 없지만 관련 기능을 수행하기 위해서는 다음 내장객체들을 전부 요청받아야 하는 것이다.

 

private final MemoRepository memoRepository;

우선 LocalTimeDate 프로퍼티 값을 참조하기 위해서 당연히 MemoRepository.java 클래스를 final로 받아야 하고..

저 상태로는 에러가 뜨기 때문에 @RecuiredArgsConstructor를 선언해줬다.

 

@Transactional
public Long update(Long id, MemoRequestDto requestDto){
    Memo memo = memoRepository.findById(id).orElseThrow(
            () -> new IllegalArgumentException("해당 아이디가 존재하지 않습니다!")
    );

    memo.update(requestDto);
    return memo.getId();
}

요 코드블록이 해당 페이지의 핵심기능이라고 할 수 있겠다.

서버 DB와의 연동을 위해 @Transactional을 선언 해주었다.

 

public Long update() {}는 별도로 선언한 메서드인데

Column No. 에 해당하는 Long 타입의 id 변수와

MemoRequestDto 내장객체에서 참조한 username / contents 변수를 모두 업데이트 해주겠다는 뜻이다.

 

public void update(MemoRequestDto requestDto){
    this.username = requestDto.getUsername();
    this.contents = requestDto.getContents();
}

앞단 Memo.java 클래스에서 쓰인 void 타입 update 메서드가 바로 여기에서 쓰인다고 할 수 있다.

코드 블록 하단 memo.update(requestDto)가 리턴해주고 있다.

 

Memo memo = memoRepository.findById(id).orElseThrow(
        () -> new IllegalArgumentException("해당 아이디가 존재하지 않습니다!")
);

update 메서드 안에 들어가있는 내용물을 살펴보자.

레퍼지토리를 참조하여 id별로 전부 조회한 다음에..

해당 id, 즉 Column No.가 존재하지 않은다면

() -> IllegalArgumentException()으로 예외처리를 해주겠다는 뜻이다.

그걸 memo에 저장해주려는 거고..

 

 

말 그대로 별도의 서비스를 담당하기 위한 패키지이다.

 

자 이제 Controller 하나만 남았다.

조금 더 힘을 내보자!


1-3) Controller

  • MemoController.java

 

본격적으로 코드리뷰를 하기 전에 다시 한 번 라미인딩 해보자.

이번 타임라인 서비스뿐만 아니라 MVC 패턴으로 만든 게시판에 없어서는 안되는 기능이 뭐지?

C.R.U.D이다.

같은 기능을 수행하는 녀석들이 GET (READ) / POST (CREATE) / PUT (UPDATE) / DELETE (DELETE) 였지?

MemoController.java 클래스는 바로 서버 API 기능을 직접적으로 수행하기 위한 녀석이다.

이번에도 맥도날드를 예로 들어본다면 무엇이 적절할까..?

옳거니! 카운터 알바생 정도가 딱 맞겠다.

 

import com.sparta.timeline.domain.Memo;
import com.sparta.timeline.domain.MemoRepository;
import com.sparta.timeline.domain.MemoRequestDto;
import com.sparta.timeline.service.MemoService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.List;

보셈..

주방이건 홀이건 배달이건 걸치지 않은 곳이 없잖슮?

어쩌면 도메인보다 이놈이 더 바쁘겠네..

 

@RestController
@RequiredArgsConstructor

public class MemoController {

	(생략)
    
    }

전체적은 구성만 놓고 본다면 코드 자체는 그리 복잡하지 않다.

오히려 여태까지 올린 코드블록 중에 가장 단순하고 직관적이라고 할 수 있겠다.

@RestController는 서비스와 마찬가지로 자바에게 이 페이지가 컨트롤러 역활을 담당하고 있을을 알려주는 어노테이션이다.

저 MemoCotroller 클래스에서 모든 API가 요청과 응답을 반복하고 있는 것이다.

 

"홀에 슈슈버서 세트 하나요~!"

"주문하신 슈슈버거 세트 나왔습니다~!"

 

이런 기능을 수행하고 있는 셈이다.

이렇게 보니까 하나도 어렵지 않지?

 

@GetMapping("/api/memos")
public List<Memo> getMemos(){
    LocalDateTime start = LocalDateTime.of
            (LocalDate.now().minusDays(1), LocalTime.of(0,0,0));
    // Yesterday: Before 24 Hour

    LocalDateTime end = LocalDateTime.of(LocalDate.now(), LocalTime.of(23,59,59));
    // Today: Current Time

    return memoRepository.findAllByModifiedAtBetweenOrderByModifiedAtDesc(start, end);
}

가장 먼저 파라미터 값을 READ하기 위한 Mapping 구간이다.

@GetMapping으로 인해 해당 생성자는 API에서 GET 기능을 수행할 것이다.

Memo.java 클래스의 모든 변수들을 GET 해오는 것은 물론이고..

LocalDateTime을 선언하여 조회시간으로부터 24시간 전 까지의 데이터들을 모두 불러오겠다는 거다.

LocalDateTime을 참조하기 위해 스프링에서 자체적으로 TimeStamped.java 클래스를 임포트 해줬다.

리턴을 어떤 값으로 받고 있는지 주의깊게 살펴보길 바란다.

 

@PostMapping("/api/memos")
public Memo createMemo(@RequestBody MemoRequestDto requestDto){
    Memo memo = new Memo(requestDto);

    return memoRepository.save(memo);
}

// UPDATE
@PutMapping("/api/memos/{id}")
public Long updateMemo(@PathVariable Long id, @RequestBody MemoRequestDto requestDto){
    // Memo memo = new Memo();
    return memoService.update(id, requestDto);
}

// DELETE
@DeleteMapping("/api/memos/{id}")
public Long DeleteMemo(@PathVariable Long id){
    memoRepository.deleteById(id);
    return id;
}

GET을 설정해줬으니 나머지 POST / PUT / DELETE도 빠져서는 안되겠지?

아! RequestDto 내장객체를 참조하기 위해서는 별도로 @RequestBody라고 어노테이션을 선언해줘야 한다.

@PathVariable은 Long 타입의 id 값을 경로로 삼겠다는 뜻이다.

 

POST 기능이 제대로 수행되기 위해서는 최종적으로 리퍼지토리를 저장해주기 위해 *.save(memo)를 리턴받아야 할 것이며..

 

PUT은 UPDATE, 즉 타임라인 수정기능이 수행되어야 한다.

미리 만들어놓은 MemoService를 번지로 삼아 각각 id 값과 requestDto에 실린 데이터들을 업데이트 해줘야 한다.

리턴타임을 풀어쓴거다.

 

DELETE는 더 볼 것도 없이 타임라인 삭제기능을 수행하고 있다.

컬럼 전체를 삭제해주려고 하나보다.

 


 

이상으로 서버단 코드리뷰를 모두 마치겠다.

장장 3일에 걸쳐 포스팅한 내용들이라 분량이 엄청나다.

이렇게 리뷰에 심혈을 기울인 이유는..

깃-허브 readMe를 대신 해준 것이다.

가독성은 블로그가 훨씬 좋을거 아니야?

 

문제는 여기서 끝이 아니라는거다.

아직 클라이언트가 남아있다.

하아..

 


2. Application [Client] Scope

여기까지 정말 숨가쁘게 달려왔는데..

글 초입에 말했듯이 필자는 아직 프론트쪽 지식이 전무하다시피 하다.

HTML과 JSP 정도는 구현할 줄 알지만

그것만으로는 클라이언트를 예쁘게 꾸밀 수가 없다.

[JSP]

솔직히 이 정도가 필자의 현 주소이다.

누가 웹 어플리케이션을 저따구로만 구현하겠는가?

웹에 생명력을 불어넣어 줄 CSS와 JavaScript는 전무한 형편이다.

 

하지만 뭐 할 수 없는 걸 할 수 있다고 구라를 칠 수는 없는 거잖아?

게다가 이번에 듣는 "웹 개발의 봄, Spring!"은 서버 구현 쪽에 집중된 수업이다.

그렇기 때문에 CSS 부분은 복사+붙여넣기로 대체할 수 밖에 없는 점을 이해 바란다.

대신 프론트 단의 전체 구조와, 동적 웹페이지를 구성하기 위한 JavaScript 문법 정도만 살펴보도록 하자.

 

우선은 우리가 구현하고자 하는 클라이언트의 구조부터 파헤쳐보겠다.

 

  1. 필요한 기능 살펴보기
    • 접속하자마자 메모 전체 목록 조회하기
      1. GET API 사용해서 메모 목록 불러오기
      2. 메모 마다 HTML 만들고 붙이기
    • 메모 생성하기
      1. 사용자가 입력한 메모 내용 확인하기
      2. POST API 사용해서 메모 신규 생성하기
      3. 화면 새로고침하여 업데이트 된 메모 목록 확인하기
    • 메모 변경하기
      1. 사용자가 클릭한 메모가 어떤 것인지 확인
      2. 변경한 메모 내용 확인
      3. PUT API 사용해서 메모 내용 변경하기
      4. 화면 새로고침하여 업데이트 된 메모 목록 확인하기
    • 메모 삭제하기
      1. 사용자가 클릭한 메모가 어떤 것인지 확인
      2. DELETE API 사용해서 메모 삭제하기
      3. 화면 새로고침하여 업데이트 된 메모 목록 확인하기

 

키워드가 되는 단어마다 형광팬으로 예쁘게 색칠해놨다.

일찍히 API란 서버와 클라이언트 간의 약속이라고 한 적이 있다.

컨텐츠를 CREATE / READ / UPDATE / DELETE 해주기 위해서

이 둘은 JSON 방식으로 요청과 응답을 주고받아야 한다.

 

그렇다면 지금부터는 이런 기능들이 HTML 화면에서 제대로 작성이 되었는지를 확인해보도록 하자.

다음 코드들은 여태까지 JAVA에서 써왔던 문법과는 판이한

JavaScript 내지 jQuery를 위주로 작성한 것들이기 때문에 그냥 뇌를 비워주기 바란다.

 

첫 번째 코드 나가신다..!

 

$(document).ready(function () {
    // HTML 문서를 로드할 때마다 실행합니다.
    getMessages();
})

저 달러표기..

자바에서는 Expression Language라고 해서 내장객체를 아주 손쉽게 불러올 수 있는 녀석인데..

jQuery에서는 크게 연관성이 없어 보인다..ㅠㅠㅠ

새로고침 영역 정도로만 이해해라.

 

// 메모를 불러와서 보여줍니다.
function getMessages() {
    // 1. 기존 메모 내용을 지웁니다.
    $('#cards-box').empty();
    // 2. 메모 목록을 불러와서 HTML로 붙입니다.
    $.ajax({
        type: 'GET',
        url: '/api/memos',
        success: function (response) {
            for (let i = 0; i < response.length; i++) {
                let memo = response[i];
                let id = memo.id;
                let username = memo.username;
                let contents = memo.contents;
                let modifiedAt = memo.modifiedAt;
                addHTML(id, username, contents, modifiedAt);
            }
        }
    })
}

100% 이해할 수는 없지만 memo를 GET (READ) 해주기 위한 함수인 것 정도는 눈에 보인다.

특히 저 for문은 JS라고 크게 다른 게 아니다.

변수 그 자체는 익숙하지?

우선은 읽을 수 있다는 것에 만족하자..ㅠㅠㅠㅠ

 

// 메모 하나를 HTML로 만들어서 body 태그 내 원하는 곳에 붙입니다.
    function addHTML(id, username, contents, modifiedAt) {
        // 1. HTML 태그를 만듭니다.
        let tempHtml = `<div class="card">
    <!-- date/username 영역 -->
    <div class="metadata">
        <div class="date">
            ${modifiedAt}
        </div>
        <div id="${id}-username" class="username">
            ${username}
        </div>
    </div>
    <!-- contents 조회/수정 영역-->
    <div class="contents">
        <div id="${id}-contents" class="text">
        <!-- *-contents: contents READ -->
            ${contents}
        </div>
        <div id="${id}-editarea" class="edit">
            <textarea id="${id}-textarea" class="te-edit" name="" id="" cols="30" rows="5"></textarea>
            <!-- *-textarea: contents PUT -->
        </div>
    </div>
    <!-- 버튼 영역-->
    <div class="footer">
        <img id="${id}-edit" class="icon-start-edit" src="images/edit.png" alt="" onclick="editPost('${id}')">
        <img id="${id}-delete" class="icon-delete" src="images/delete.png" alt="" onclick="deleteOne('${id}')">
        <img id="${id}-submit" class="icon-end-edit" src="images/done.png" alt="" onclick="submitEdit('${id}')">
    </div>
</div>`;

 

 

// 2. #cards-box 에 HTML을 붙인다.
    $('#cards-box').append(tempHtml);
}

 

// 메모를 생성합니다.
function writePost() {
    // 1. 작성한 메모를 불러옵니다.
    let contents = $('#contents').val();
    // 2. 작성한 메모가 올바른지 isValidContents 함수를 통해 확인합니다.
    if (isValidContents(contents) == false) {
        return;
    }
    // 3. genRandomName 함수를 통해 익명의 username을 만듭니다.
    let username = genRandomName(10);
    // 4. 전달할 data JSON으로 만듭니다.
    let data = {'username': username, 'contents': contents};
    // 5. POST /api/memos 에 data를 전달합니다.
    $.ajax({
        // $.ajax({}): Willing to use jQuery
        type: "POST",
        url: "/api/memos",
        contentType: "application/json", // JSON 형식으로 전달함을 알리기, 번역 잘하라고~
        data: JSON.stringify(data),
        // data (Body in ARC)
        // JSON.stringify():  JavaScript 값이나 객체를 JSON 문자열로 변환해주는 메서드
        success: function (response) {
            alert('메시지가 성공적으로 작성되었습니다.');
            window.location.reload();
            // 새로고침 F5
        }
    });
}

여기는 딱 봐도 POST (CREATE) 영역이겠구만..

 

// 메모를 수정합니다.
function submitEdit(id) {
    // 1. 작성 대상 메모의 username과 contents 를 확인합니다.
    let username = $(`#${id}-username`).text().trim();
    let contents = $(`#${id}-textarea`).val().trim();
    // 2. 작성한 메모가 올바른지 isValidContents 함수를 통해 확인합니다.
    if (isValidContents(contents) == false) {
        return;
    }
    // 3. 전달할 data JSON으로 만듭니다.
    let data = {'username': username, 'contents': contents};
    // 4. PUT /api/memos/{id} 에 data를 전달합니다.
    $.ajax({
        type: "PUT",
        url: `/api/memos/${id}`,
        contentType: "application/json",
        // Hey, Java! this is not String type. It's JSON!
        data: JSON.stringify(data),
        success: function (response) {
            alert('메시지 변경에 성공하였습니다.');
            window.location.reload();
        }
    });
}

요 놈은 데이터를 수정하기 위한 PUT (UPDATE) 영역이 되시겠다.

 

// 메모를 삭제합니다.
function deleteOne(id) {
    $.ajax({
        type: "DELETE",
        url: `/api/memos/${id}`,
        // Backtick(``)을 사용하지 않으면 문자열 그대로 전송이 되니 유의할 것!
        success: function (response) {
            alert('메시지 삭제에 성공하였습니다.');
            window.location.reload();
        }
    })
}

마지막은 데이터를 삭제하기 위한 DELETE 영역!


 

여기까지가 필자가 프론트에서 그나마 깔짝댈 만한 JavaScript 문법 영역이다.

백엔드만 하더라도 갈 길이 너무나도 먼데 앞단까지 손 대기에는 필자가 코딩을 시작한지 4개월밖에 안됐다.

더 정진해서 유익한 글 많이 많이 올릴테니까..

 

구독과 좋아요는 사랑이에요 헤으응..