[Spring] Spring 패키지 구조

Ogu·2023년 6월 17일
0

SpringBoot

목록 보기
14/17

스프링부트는 객체지향적인 설계를 위해 목적에따라 여러 패키지로 분류하여 코드를 관리한다.
spring boot의 패키지 구조를 살펴보고 각 기능별로 살펴보자.

📟 패키지 구조

스프링 패키지 구조는 기본적으로 다음과 같이 이루어져 있다.

  • Controller
  • DTO
  • Service
  • Repository
  • Domain (Entity)
    하나씩 살펴보도록 하자.

🎈 계층형 구조, 도메인형 구조

계층형 구조

계층형으로 패키지를 설계하는 방식이다.
전체적인 구조를 빠르게 파악할 수 있지만 디렉토리에 클래스들이 너무 많이 모이는 단점이 있다.

도메인형 구조

  • 도메인 단위로 디렉토리를 구성하는 방식이다.
  • 관련된 코드들이 응집해있는 장점이 있지만, 프로젝트에 대한 이해도가 낮을 경우 전체 구조를 파악하기 어렵다.

🎈 Controller

HTTP 요청과 응답을 위한 클래스로서, 제일 앞단에서 동작한다고 볼 수 있다. @Controller 어노테이션을 사용해 bean에 등록하여 스프링에서 관리하는 객체가 됩니다.

🎈 Service

Repository와 DTO를 통해 db에 접근하여 데이터 CRUD등의 관리와 에러 처리 등등을 담당한다.
Domain과 DTO를 나눈 것과 마찬가지로 Service와 Controller를 나눈 이유는 다음과 같다.

  • 중복 코드가 발생하면 모듈화를 통해 나눠주어 유지 보수를 하기 편하다.
  • 기능을 세분화 하여 Service에 등록하면 추후 기능을 조합하기만 해서 새로운 기능을 만들 수 있다.
  • Service에서 다른 Service를 의존성 참조하는 것도 가능하다.

즉 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);
    }

}

🎈 Domain

DB 테이블과 직접 맵핑되는 클래스로서 JPA 사용 시 어노테이션을 이용하여 테이블, 필드, 등을 설정한다.
Domain을 DTO와 분리해서 사용ㅎ는 이유는 다음과 같다.

  • Client 쪽과 연결된 부분은 잦은 변경사항이 있을 수 있는데 Domain과 연결되어 자주 변경되게 된다면 여러 클래스에 영향을 미치기 때문에 분리한다.
  • DTO는 Domain Model을 복사한 형태로 다양한 Presentation Logic을 추가한 정도로 사용하며 Domain Model 객체는 Persistent만을 위해서 사용한다.
  • View 단과 DB 단을 확실하게 분리하기 위해서 사용한다.
@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

  • 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
        );
    }
}

🎈 Repository

  • DB에 접근하는 코드를 모아둔 Interface이다.
  • JpaRepository interface를 상속받아서 관리하고자 하는 클래스, ID 필드 타입을 JpaRepository<Entity Class, PK type> 같이 넣어주면 자동으로 DB와 CRUD 연결을 할 수 있는 메소드를 생성해준다.
  • Create, Updeate, Delete와 같은 경우에는 다른 테이블 간의 조인을 잘 수행하지 않기 때문에 그대로 사용하지만 Read와 같은 경우에는 여러 테이블과 조인 연산이 필요한 경우가 많기 때문에 @Query 어노테이션으로 직접 쿼리를 작성하여 사용하거니 querydsl과 같은 동적 쿼리를 생성해주는 라이브러리를 같이 사용하게 된다.
public interface MemberRepository extends JpaRepository<Member, Long> {

}

📟 폴더 구조

🎈 src/main/java

자바(.java) 파일이 모여있는 곳이다. 패키지로 분리하여 자바 클래스를 생성해 사용하면 된다. 스프링에서 이미 MVC 패턴의 서블릿 구조를 잡아주기 때문에 따로 서블릿을 만들 필요 없이 스프링 구조에 맞춰 클래스 파일들을 작성한다.
Controller, Service, Repository, Entity, DTO등이 들어간다.

🎈 src/main/resources

자바를 제외한 HTML, CSS, JavaScript, DB연결을 위한 자원, 의존성 주입(DI)를 위한 설정 파일 등 Application/yml/xml 등의 파일을 작성하는 곳이다.

static

스타일시트(css), 자바스크립트(js) 그리고 이미지 파일(jpg, png)등을 저장하는 공간이다.

tamplates

템플릿 파일을 저장한다. 템플릿 파일은 HTML 파일 형태로 자바 객체와 연동되는 파일이다.

🎈 src/test

테스트를 위한 자바 코드와 리소스를 보관하는 곳이다.

참고

profile
私はゲームと日本が好きなBackend Developer志望生のOguです🐤🐤

0개의 댓글