스프링부트는 객체지향적인 설계를 위해 목적에따라 여러 패키지로 분류하여 코드를 관리한다.
spring boot의 패키지 구조를 살펴보고 각 기능별로 살펴보자.
스프링 패키지 구조는 기본적으로 다음과 같이 이루어져 있다.
계층형으로 패키지를 설계하는 방식이다.
전체적인 구조를 빠르게 파악할 수 있지만 디렉토리에 클래스들이 너무 많이 모이는 단점이 있다.
HTTP 요청과 응답을 위한 클래스로서, 제일 앞단에서 동작한다고 볼 수 있다. @Controller
어노테이션을 사용해 bean에 등록하여 스프링에서 관리하는 객체가 됩니다.
Repository와 DTO를 통해 db에 접근하여 데이터 CRUD등의 관리와 에러 처리 등등을 담당한다.
Domain과 DTO를 나눈 것과 마찬가지로 Service와 Controller를 나눈 이유는 다음과 같다.
즉 Service를 사용하는 이유는 확장성, 재사용성, 중복 코드의 제거이다.
@Slf4j
@RequiredArgsConstructor
@Transactional
@Service
public class ArticleService {
private final ArticleRepository articleRepository;
@Transactional(readOnly = true)
public Page<ArticleDto> searchArticles(SearchType searchType, String searchKeyword, Pageable pageable) {
if (searchKeyword == null || searchKeyword.isBlank()) {
return articleRepository.findAll(pageable).map(ArticleDto::from);
}
return switch (searchType) {
case TITLE ->
articleRepository.findByTitleContaining(searchKeyword, pageable).map(ArticleDto::from);
case CONTENT ->
articleRepository.findByContentContaining(searchKeyword, pageable).map(ArticleDto::from);
case ID ->
articleRepository.findByUserAccount_UserIdContaining(searchKeyword, pageable).map(ArticleDto::from);
case NICKNAME ->
articleRepository.findByUserAccount_NicknameContaining(searchKeyword, pageable).map(ArticleDto::from);
case HASHTAG ->
articleRepository.findByHashtag("#" + searchKeyword, pageable).map(ArticleDto::from);
};
}
@Transactional(readOnly = true)
public ArticleWithCommentsDto getArticle(long articleId) {
return articleRepository.findById(articleId)
.map(ArticleWithCommentsDto::from)
.orElseThrow(() -> new EntityNotFoundException("게시글이 없습니다 - articleId: " + articleId));
}
public void saveArticle(ArticleDto dto) {
articleRepository.save(dto.toEntity());
}
public void updateArticle(ArticleDto dto) {
try {
Article article = articleRepository.getReferenceById(dto.id());
if (dto.title() != null) { article.setTitle(dto.title()); }
if (dto.content() != null) { article.setContent(dto.content()); }
article.setHashtag(dto.hashtag());
// articleRepository.save(article);
// save 따로 작성 x -> class level 트랜잭션에 의해서 메소드 단위로 트랜잭션이 묶여있음.
// 따라서 트랜잭션이 끝날 때 영속성 컨택스트는 Article이 변한 것을 감지함
// -> 쿼리를 날림 (update 쿼리가 실행됨)
} catch (EntityNotFoundException e) {
log.warn("게시글 업데이트 실패. 게시글을 찾을 수 없습니다 - dto: {}", dto);
}
}
public void deleteArticle(long articleId) {
articleRepository.deleteById(articleId);
}
}
DB 테이블과 직접 맵핑되는 클래스로서 JPA 사용 시 어노테이션을 이용하여 테이블, 필드, 등을 설정한다.
Domain을 DTO와 분리해서 사용ㅎ는 이유는 다음과 같다.
@Entity
public class Article extends AuditingFields{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // mysql의 autoincreasement는 IDENTITY
private Long id;
@Setter @ManyToOne(optional = false) private UserAccount userAccount; // 유저 정보 (ID)
@Setter @Column(nullable = false) private String title; // 제목
@Setter @Column(nullable = false, length = 10000) private String content; // 내용
// nullable은 true가 default값
@Setter
private String hashtag; // 해시태그
// One to Many - Coment들을 중복을 허용하지 않고 모아서 보여줌
@OrderBy("createdAt DESC")
@OneToMany(mappedBy = "article", cascade = CascadeType.ALL)
@ToString.Exclude
private final Set<ArticleComment> articleComments = new LinkedHashSet<>();
protected Article() {
}
private Article(UserAccount userAccount, String title, String content, String hashtag) {
this.userAccount = userAccount;
this.title = title;
this.content = content;
this.hashtag = hashtag;
}
public static Article of(UserAccount userAccount, String title, String content, String hashtag) {
return new Article(userAccount, title, content, hashtag);
}
DTO는 Data Transfer Object로 "데이터 전송 객체"이다.
Service나 Controller에서 DB에 접근할 때 사용하는 클래스로, Domain은 DB 테이블에 대한 정보를 가지고 있는 클래스이고, DTO는 해당 테이블에서 실제로 CRUD를 할 필드를 정의해둔 것이라고 보면 된다.
따라서 테이블에 대한 정보를 작성하는 Domain 클래스와 DB에 접근하는 필드에 관한 내용을 작성하는 DTO 클래스를 사용하며 Domain과 마찬가지로 Builder 패턴을 사용할 수 있다.
public record ArticleDto(
Long id,
UserAccountDto userAccountDto,
String title,
String content,
String hashtag,
LocalDateTime createdAt,
String createdBy,
LocalDateTime modifiedAt,
String modifiedBy
) {
public static ArticleDto of(Long id, UserAccountDto userAccountDto,
String title, String content, String hashtag,
LocalDateTime createdAt, String createdBy,
LocalDateTime modifiedAt, String modifiedBy) {
return new ArticleDto(id, userAccountDto, title, content,
hashtag, createdAt, createdBy, modifiedAt, modifiedBy);
}
public static ArticleDto from(Article entity) {
return new ArticleDto(
entity.getId(),
UserAccountDto.from(entity.getUserAccount()),
entity.getTitle(),
entity.getContent(),
entity.getHashtag(),
entity.getCreatedAt(),
entity.getCreatedBy(),
entity.getModifiedAt(),
entity.getModifiedBy()
);
}
public Article toEntity() {
return Article.of(
userAccountDto.toEntity(),
title,
content,
hashtag
);
}
}
JpaRepository<Entity Class, PK type>
같이 넣어주면 자동으로 DB와 CRUD 연결을 할 수 있는 메소드를 생성해준다.@Query
어노테이션으로 직접 쿼리를 작성하여 사용하거니 querydsl
과 같은 동적 쿼리를 생성해주는 라이브러리를 같이 사용하게 된다.public interface MemberRepository extends JpaRepository<Member, Long> {
}
src/main/java
자바(.java) 파일이 모여있는 곳이다. 패키지로 분리하여 자바 클래스를 생성해 사용하면 된다. 스프링에서 이미 MVC 패턴의 서블릿 구조를 잡아주기 때문에 따로 서블릿을 만들 필요 없이 스프링 구조에 맞춰 클래스 파일들을 작성한다.
Controller, Service, Repository, Entity, DTO등이 들어간다.
자바를 제외한 HTML, CSS, JavaScript, DB연결을 위한 자원, 의존성 주입(DI)를 위한 설정 파일 등 Application/yml/xml 등의 파일을 작성하는 곳이다.
스타일시트(css), 자바스크립트(js) 그리고 이미지 파일(jpg, png)등을 저장하는 공간이다.
템플릿 파일을 저장한다. 템플릿 파일은 HTML 파일 형태로 자바 객체와 연동되는 파일이다.
테스트를 위한 자바 코드와 리소스를 보관하는 곳이다.