본문 바로가기
느낌점

장고와 스프링으로 넘어가면서 미리 알았으면 좋았을 것 정리

by sungin95 2023. 8. 25.

Django에서 프로젝트도 여러번 해 보고 더 큰 취업에 문을 열기 위해 Spring+intelliJ으로 넘어간지 두달 정도 된거 같습니다. 

오늘은 만약 저랑 같이 장고를 배웠던 동기가 스프링을 배우기 시작한다면 알려주고 싶은 것들을 정리해 보았습니다. 

간단한 게시판 서비스를 예시로 설명하겠습니다. 

 

틀린점이 있다면 댓글로 알려주시면 감사하겠습니다ㅠ

 

장고 구조와 스프링 구조

장고는 Request요청이 들어오면 urls.py파일에서 매칭되는 view함수를 찾게 되고. 

view함수에서는 Model과 상호작용을 한 후 그 값을 response응답 해 줍니다. 

간단하게 코드로 표현해 보면

# urls.py
from django.urls import path
from . import views

urlpatterns = [
    path("today/", views.index),
]
# models.py
from django.db import models
from django.conf import settings

class Article(models.Model):
    user_id = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    title = models.CharField(max_length=500)
    content = models.TextField()
# views.py
def index(request):
	articles = Article.objects.all()
    
    context = {
        "articles": articles,
    }
    return render(request, "index.html", context)

정말 간단하게 표현 할 수 있다. 

 

그에 반해 스프링 구조도는 딱 봐도 복잡하다. 

이 중에서 직접 다루는 부분인 오른쪽 빨간 박스에 부분에 대해 다루겠습니다. 

 

urls.py, views.py, models.py는 어디에 갔나?

처음에 공부하면서 가장 당황스러웠던 점은 내가 알던 모든게 없다는 점이었습니다. 

순서에 맞추어 설명을 드리자면

 

urls.py

스프링에서는 url 파일을 따로 두지 않고 컨트롤러에서 관리를 합니다. 

그리고 어노테이션(Annotation)이란 걸 사용해서 관리합니다. 

어노테이션은 사전적 의미로는 주석이라는 뜻으로 `@GetMapping`처럼 맨 앞에 @를 붙여서 사용하는 특별한 의미, 기능을 수행하도록 하는 기술입니다. 

장고와는 다르게 스프링에서는 이 기술이 정말 많이 사용됩니다. 

예를들어 URL을 어노테이션을 통해 관리를 합니다. 

// ArticleController
...

@RequiredArgsConstructor
@RequestMapping("/articles") // url 표시
@Controller
public class ArticleController {
    private final ArticleService articleService;
    private final PaginationService paginationService;
    
    ...
    
    @GetMapping("/{articleId}") //GET메서드로 /articles/{articleId} URL 호출 시 나오게 된다
    public String article(@PathVariable Long articleId, ModelMap map) {
        ArticleWithCommentsResponse article = ArticleWithCommentsResponse.from(articleService.getArticleWithComments(articleId));

        map.addAttribute("article", article);
        map.addAttribute("articleComments", article.articleCommentsResponse());
        map.addAttribute("totalCount", articleService.getArticleCount());
        return "articles/detail";
    }
    
    ...

또한 모델의 필드의 옵션을 설정할 때도 사용 됩니다. 

package com.fastcampus.projectboard.domain;

import lombok.*;


import javax.persistence.*;
import java.util.LinkedHashSet;
import java.util.Objects;
import java.util.Set;

@Getter
@ToString(callSuper = true)
@Table(indexes = {
        @Index(columnList = "title"),
        @Index(columnList = "hashtag"),
        @Index(columnList = "createdAt"),
        @Index(columnList = "createdBy"),
})
@Entity
public class Article extends AuditingFields {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id; // pk설정

    @Setter
    @ManyToOne(optional = false) // ForeignKey
    @JoinColumn(name = "userId")
    private UserAccount userAccount; 

    @Setter
    @Column(nullable = false) //null=True
    private String title; // 제목
    
    @Setter
    @Column(nullable = false, length = 10000) //null=True, max_length=10000
    private String content; // 본문

	...
    ...

}

이 어노테이션을 통해 정말 다양한 것을 스프링에서는 표현을 합니다. 

그래도 다행히인 점은 모르는 어노테이션이 나와도 구글에 검색을 하면 관련 정보가 많이 나와 있습니다. 

 

models.py

Spring에서는 이 부분이 크게 3가지로 나눌 수 있습니다. 

뼈대를 설정하는 domain

저장소 repository 

실질적으로 활용할 때 사용하는 dto

 

이 부분도 처음에는 많이 헷갈렸습니다. 장고에서는 한 파일에서 모델 필드를 설정하고 그 모델 클래스를 통해서 인스턴스를 불러 왔는데. 스프링에서는 다 나누어서 사용을 했거든요.

나누었다는 그 자체보다 이 구조를 파악하는게 어려웠습니다. 

 

설명을 드리자면

뼈대를 설정하는 domain에서는 모델의 필드와 설정을 정해 줍니다. 

package com.fastcampus.projectboard.domain;

import lombok.*;


import javax.persistence.*;
import java.util.LinkedHashSet;
import java.util.Objects;
import java.util.Set;

@Getter
@ToString(callSuper = true)
@Table(indexes = {
        @Index(columnList = "title"),
})
@Entity
public class Article {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Setter
    @ManyToOne(optional = false)
    @JoinColumn(name = "userId")
    private UserAccount userAccount;

    @Setter
    @Column(nullable = false)
    private String title; // 제목
    @Setter
    @Column(nullable = false, length = 10000)
    private String content; // 본문


    @ToString.Exclude
    @OrderBy("createdAt DESC")
    @OneToMany(mappedBy = "article", cascade = CascadeType.ALL)
    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;
    }

    public static Article of(UserAccount userAccount, String title, String content) {
        return new Article(userAccount, title, content);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Article that)) return false;
        return id != null && id.equals(that.getId());
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }
}

그리고 인스턴스를 불러올때는 repository를 활용합니다.

// ArticleService

@Slf4j
@RequiredArgsConstructor
@Transactional
@Service
public class ArticleService {

    private final ArticleRepository articleRepository;

    @Transactional(readOnly = true)
    public Page<ArticleDto> searchArticles() {

		// articleRepository.findAll()와 장고의 Article.objects.all()는 같다.
        return articleRepository.findAll().map(ArticleDto::from);
        
    }

 

 

그리고 위 코드를 보면 ArticleDto 라는게 나오는데. 

Dto를 검색해서 찾아보면 `DTO는 Data Transfer Object의 약자로, 계층 간(Controller, View, Business Layer) 데이터 교환을 위한 Java Ben을 의미한다. DTO는 로직을 가지지 않는 데이터 객체이고, getter, setter 메소드만 가진 클래스를 의미한다.` 고 써있는데. 

저는 이 말은 별로 와 닿지 않아서 제 나름대로 표현을 해 보자면 결국 로직을 짜다 보면 모든 필드가 다 필요로 하지 않는데. 그것을 자유롭게 커스텀 마이징해서 사용 할 수 있도록 만들었다 입니다. 

 

이걸 활용해 보자면 게시글 작성을 위해서는 제목, 본문 내용, 작성자 3가지 정보가 필요하다.

그런데 작성자는 따로 적는게 아니라 로그인 한 유저로 부터 받아야 한다.

그래서 제목과 본문 내용만 받는 dto와 유저 Dto를 만들어 준다. 

그리고 이 둘을 합치는 dto를 만들어 주어 관리를 한다. 

 

말로 설명하기 너무 힘들어서 그림으로 준비해 보았습니다.

RequestArticleDto 는 제목과 본문 내용 받도록 설정

UserAccountDto는 로그인 유저를 받도록 설정

이 둘을 합치면 ArticleDto가 완성

그리고 이 ArticleDto를 통해서 최종적으로 Article을 생성 할 수 있습니다. 

 

이 처럼 DTO를 다양하게 커스텀 해서 코드에서 활용을 할 수 있게 해 줍니다. 

 

views.py

이 부분은 Controller와 Service 부 부분으로 나뉘어 집니다. 

이 부분은 간단하게 설명드리면

# views.py
def index(request):
	
	articles = Article.objects.all()
    
    context = {
        "articles": articles,
    }
    return render(request, "index.html", context)

Service부분에서 실질적인 로직을 관리하고 

    @Transactional(readOnly = true)
    public Page<ArticleDto> sgetAllArticles() {

        return articleRepository.findAll().map(ArticleDto::from);
        
    }

Controller부분은 적절한 Service를 불러와서 데이터를 client로 보내 주는 역할이라고 요약을 해보 충분할 거 같습니다. 

    @GetMapping
    public String articles(
            ModelMap map //context와 같은 역할
    ) {
        Page<ArticleResponse> articles = articleService.getAllArticles().map(ArticleResponse::from);
        map.addAttribute("articles", articles);
        
        return "articles/index";

 

 

그외...

viscode -> IntelliJ

node.js를 찍먹 할 때도, Django에서도, 리액트를 배울때도 vicode를 사용했는데. java로 넘어가면서 IntelliJ를 사용하게 되었습니다. 협업에서 ultimate를 사용한다고 해서 1년 사용권으로 대략 20만원정도를 사용한거 같습니다.ㅠ(다행히 1년치를 사면 해당 버전은 계속 사용이 가능한거 같습니다.)

 

패키지

패키지는 Django에서는 하나의 views.py 파일에 여러 함수를 모두 적어 놓았다면 Spring에서는 controller라는 패키지 폴더를 생성한 다음 그 안에 여러개의 파일을 만들어서 한 파일에 한개의 함수를 만들어서 관리를 합니다. 

 

 

venv 파일 추가

의존성 추가라고 부르고 build.gradle이라는 파일에서 추가를 합니다. 

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-data-rest'
    implementation 'org.springframework.data:spring-data-rest-hal-explorer'
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    runtimeOnly 'com.h2database:h2'
    runtimeOnly 'com.mysql:mysql-connector-j'
    compileOnly 'org.projectlombok:lombok'
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

 

ORM

장고에서는 따로 ORM을 어떤 프로그램을 사용할지 지정을 안해 줬는데. Spring에서는 따로 의존성을 추가하여 지정해 줘야 하고 주로 JPA를 사용합니다. (ORM프로그램을 지정한 적이 없어서 JPA가 뭐지 한참 헷갈렸는데. JAVA의 ORM인걸 알고 허탈)

 

 

Makemigrations, migrate

이건 JAVA가 컴파일 언어라서인지 따로 없었습니다. 

 

느낌점.

Django에서 Spring으로 넘어가는 것이 생각보다 많이 어려웠습니다. 그리고 Django의 간편함에 대해 많이 느꼈습니다. Spring으로 넘어갈려고 할때 정말 다양한 CS지식이 필요했습니다. 그걸 모르면 이 구조가 이해가 되지를 않았거든요. 하지만 그 만큼 더 정교하게 표현을 할 수 있어서 Spring의 매력을 느낄 수 있었던거 같습니다.  

 

 

출처

내 공부 경험

https://velog.io/@ruinak_4127/Annotation%EC%9D%B4%EB%9E%80

https://velog.io/@leesomyoung/Java-DAO-DTO-VO%EC%9D%98-%EA%B0%9C%EB%85%90