2024.07.17 - [Spring] - [Spring] 스프링 빈과 의존관계-컴포넌트 스캔으로 의존관계 설정(1)

 

[Spring] 스프링 빈과 의존관계-컴포넌트 스캔으로 의존관계 설정(1)

📍스프링 빈과 의존관계  이번 시간에는 저번 포스팅에서 진행했던 유저 관리 로직을 프론트엔드와 같이 엮어 보려고 한다. 이때, 필요한것은 유저 컨트롤러와 뷰 템플릿이다. 그럴려면, 멤버

jghdg1234.tistory.com

 

이전 포스팅에는 어노테이션을 활용해 자동으로 의존 관계를 설정 해주었다. 다시 기억해 보자면... Raw 자바 코드는 Spring인지 아닌지 알 수 없으므로 @Service라는 어노테이션을 추가 해야하고, 또한, 의존성 주입을 위해 생성자 위에 @Autowired 라는 어노테이션을 추가 해야하고, 저장소에는 @Repository라는 어노테이션 등을 추가 해야한다.

 

그렇지만 이번에는, 어노테이션이 아니라 자바 코드로 직접 스프링 빈을 등록하는 방법을 알아보자.

 

먼저, src/main/java/Jihoo.hello_spring 에다가 SpringConfig라는 파일을 하나 만들어 준다.

package Jihoo.hello_spring;

import Jihoo.hello_spring.service.MemberService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SpringConfig {

    @Bean
    public MemberService memberService() {
        return new MemberService();
    }
}

 

이렇게 작성이 되면, 스프링이 시작될때 @Configuration 어노테이션과 그 안의 @Bean을 읽고, 스프링 빈에 객체를 등록 해준다.

하지만 MemverService의 생성자는 memberRepository를 인자로 받는다. 그 말은 memberRepository도 스프링 빈에 등록이 되어야 한다. 아래와 같이 memberRepository도 빈으로 생성을 해준 후, memberService의 파라미터로 던져준다.

package Jihoo.hello_spring;

import Jihoo.hello_spring.repository.MemberRepository;
import Jihoo.hello_spring.repository.MemoryMemberRepository;
import Jihoo.hello_spring.service.MemberService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SpringConfig {

    @Bean
    public MemberService memberService() {
        return new MemberService(memberRepository());
    }

    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
}

 

- MemberService와 MemberRepository를 모두 스프링 빈에 등록 한 후, 스프링 빈에 등록되어있는 memberRepo를 memberService에 넣어준다.

이렇게 하면 이전 포스팅에서 본 그림과 같이 서로 의존 관계가 형성이 되었다. 의존 관계는 실행중에 동적으로 변하는 경우는 거의 없으므로 생성자 주입을 권장한다.

 

다시 모든 내용을 정리하자면....

 

스프링 빈 (Spring Bean)이란?

스프링 빈은 스프링 애플리케이션 컨텍스트가 관리하는 자바 객체이다. 스프링 빈은 스프링 컨테이너에 의해 생성되고 관리된다. 스프링 프레임워크는 의존성 주입(Dependency Injection)을 통해 빈들의 생명주기를 관리하며, 애플리케이션의 컴포넌트 간 결합도를 낮추어 유지보수성과 테스트 용이성을 높여준다.

스프링에서는 의존성을 주입하는 방식으로 두 가지 주요 접근 방법이 있다:
1. 컴포넌트 스캔 방식 (Component Scan)
2. 직접 자바 코드로 설정하는 방식 (Java Configuration)

1. 컴포넌트 스캔 방식 (Component Scan)

스프링의 컴포넌트 스캔은 클래스에 어노테이션을 붙여 스프링 빈으로 등록하는 방식이다.

주로 `@Component`, `@Service`, `@Repository`, `@Controller`와 같은 어노테이션을 사용한다.

장점:
- 코드가 간결해지고, 설정이 쉽다.
- 클래스 파일만으로 구성 요소를 쉽게 식별할 수 있다.

단점:
- 자동으로 스캔되기 때문에 클래스 경로 내의 모든 어노테이션이 스캔되어 빈으로 등록된다.
- 어노테이션을 사용하므로 코드의 가독성이 떨어질 수 있다.

예제:

import org.springframework.stereotype.Service;

@Service
public class MemberService {
    // 서비스 로직
}


위 예제에서 `@Service` 어노테이션을 사용하여 `MemberService` 클래스가 스프링 빈으로 등록된다.

설정:
스프링 부트에서는 `@SpringBootApplication` 어노테이션이 포함된 클래스가 있는 패키지와 그 하위 패키지를 자동으로 스캔한다.

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class HelloSpringApplication {
    public static void main(String[] args) {
        SpringApplication.run(HelloSpringApplication.class, args);
    }
}


2. 직접 자바 코드로 설정하는 방식 (Java Configuration)

이 방식은 자바 클래스를 사용하여 스프링 빈을 명시적으로 등록하는 방식이다. 주로 `@Configuration`과 `@Bean` 어노테이션을 사용한다.

장점:
- 빈의 생성과 설정을 명확하게 제어할 수 있다.
- 코드 기반 설정으로 더 많은 유연성과 가독성을 제공한다.

단점:
- 설정 클래스와 메서드를 따로 작성해야 하므로 설정 파일이 많아질 수 있다.
- 컴포넌트 스캔에 비해 초기 설정 작업이 더 많다.

예제:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {

    @Bean
    public MemberService memberService() {
        return new MemberService();
    }
}


위 예제에서 `@Configuration` 어노테이션이 붙은 `AppConfig` 클래스는 스프링 설정 클래스임을 나타내며, `@Bean` 어노테이션을 사용하여 `memberService` 메서드가 반환하는 객체를 스프링 빈으로 등록한다.

설정:
스프링 부트에서 자바 설정 클래스를 사용하려면, 애플리케이션 시작 클래스에서 `@SpringBootApplication` 어노테이션과 함께 사용된다.

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Import;

@SpringBootApplication
@Import(AppConfig.class) // Java 설정 클래스 임포트
public class HelloSpringApplication {
    public static void main(String[] args) {
        SpringApplication.run(HelloSpringApplication.class, args);
    }
}



의존성 주입 방식의 차이점

컴포넌트 스캔 방식:
- 주로 어노테이션(`@Component`, `@Service`, 등)을 사용
- 설정이 간단하고 코드가 간결해짐
- 자동으로 스캔하여 빈을 등록
- 자동 스캔의 범위를 제어하기 어려움
- 스프링 부트의 `@SpringBootApplication`이 자동 스캔을 수행

자바 코드 설정 방식:
- 주로 자바 설정 클래스(`@Configuration` + `@Bean`)을 사용
- 설정과 생성을 명확하게 제어할 수 있음
- 명시적으로 빈을 등록
- 설정 파일을 통해 관리 범위를 명확히 할 수 있음
- 설정 클래스를 `@Import`를 통해 명시적으로 포함

작은 프로젝트에서는 컴포넌트 스캔 방식을, 복잡한 설정이 필요한 경우에는 자바 코드 설정 방식을 사용하는 것이 일반적이다.

 

 

📍스프링 빈과 의존관계 

 

이번 시간에는 저번 포스팅에서 진행했던 유저 관리 로직을 프론트엔드와 같이 엮어 보려고 한다. 이때, 필요한것은 유저 컨트롤러와 뷰 템플릿이다. 그럴려면, 멤버 컨트롤러를 만들어야 한다. 여기서 멤버 컨트롤러는 멤버 서비스를 통해 회원가입을 하고, 멤버 서비스를 통해 데이터를 조회 할 수 있어야 한다. 이러한 상황을 "의존관계"에 놓여 있다고 말한다. 이제, 이러한 작업을 차근차근 해 보자!

 

먼저, 멤버 컨트롤러를 만들어야 한다. 

 

이처럼 controller가 담긴 패키지 안에 멤버 컨트롤러를 만들어 준다. 여기서 한가지 알고 넘어가야 할 점은, @Controller라는 어노테이션이 붙으면, 스프링 컨테이너는 이 어노테이션을 찾아 해당되는 컨트롤러를 들고 직접 관리를 한다. 아래 컨트롤러도 마찬가지로 컨트롤러 라는 어노테이션이 참조되어 스프링 컨테이너에 의해 관리가 될 것이다.

 

package Jihoo.hello_spring.controller;

import org.springframework.stereotype.Controller;

@Controller
public class MemberController {
    
}

 

이제, 이 멤버 컨트롤러에서 멤버 서비스와 연동 할 수 있도록 안에 

private final MemberService ms = new MemberService();

 

와 같이 인스턴스를 생성 할 수 있다.

 

하지만, 스프링 컨테이너가 관리를 하게 되면, 스프링 컨테이너에 등록이 되고, 그 등록된 것들을 받아서 쓰도록 해야 한다. 즉, 여러개의 인스턴스를 생성하기 보단, 하나만 생성 해놓고 같이 공유하는 방식이다. 그래서, 이런 식으로 새로운 인스턴스를 만들기 보다는, 스프링 컨테이너에 딱 하나만 등록하여 쓰면 된다.

import Jihoo.hello_spring.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;

@Controller
public class MemberController {

    private final MemberService memberService;

    @Autowired
    public MemberController(MemberService memberService) {
        this.memberService = memberService;

    }


}

 

이런 식으로 생성자를 만들고, @Autowired 라는 어노테이션을 추가한다. 아까 언급했듯, 위에 컨트롤러 어노테이션이 붙은 MemberController는 스프링 컨테이너가 뜰때 자동으로 생성된다. 생성이 되고, 위에서 보는 생성자 호출m 하는데, 이때 생성자에  @Autowired 어노테이션이 붙어 있으면, 생성자의 파라미터로 있는 memberService에 컨테이너가 자동으로 컨테이너에 이미 존재하는 MmberService를 연결 시켜준다.

 

하지만 여기서 문제가 하나 더 발생한다. 메인 앱을 실행 해보니 콘솔에 아래와 같이 찍혔다.

 

이유를 알기 위해 MemberService 클래스를 살펴보자.

package Jihoo.hello_spring.service;

import Jihoo.hello_spring.domain.Member;
import Jihoo.hello_spring.repository.MemberRepository;
import Jihoo.hello_spring.repository.MemoryMemberRepository;

import java.util.List;
import java.util.Optional;

public class MemberService {

    private final MemberRepository memberRepository = new MemoryMemberRepository();

    /**
     * 회원 가입
     */
    public Long join(Member member) {
        //같은 이름이 있는 중복 회원 x
        Optional<Member> result = memberRepository.findByName(member.getName());
        result.ifPresent(m -> {
            throw new IllegalStateException("이미 존재하는 회원입니다.");
        });

        memberRepository.save(member);
        return member.getId();
    }

    public List<Member> findMembers() {
        return memberRepository.findAll();
    }

    public Optional<Member> findOne(Long memberId) {
        return memberRepository.findById(memberId);
    }
}

 

사실 이 클래스는 순수한 자바 코드이다. 스프링이 이 raw 자바 코드를 보고 이게 스프링인지 아닌지는 알 수 없기 때문에 MemberSerivce를 찾지 못하는 것이다. 이때, 새로운 어노테이션이 또 등장하는데, MemberService 클래스 맨 위에 @Service 라는 어노테이션을 추가하면 된다. 이렇게 서비스 어노테이션을 달아주면 스프링이 뜰때, @Service 어노테이션을 참조하여 서비스 클래스인것을 인지하고, 스프링이 스프링 컨테이너에 멤버 서비스를 자동으로 등록한다.

 

또한, 이전 포스팅에서 만들었던 MemberRepository 클래스의 상단에도 @Repository 라는 어노테이션을 달아 주어야 한다. 

 

다시 컨트롤러 얘기를 해 보자면, MemberController가 생성이 될때, 스프링 빈에 저장되어 있는 MemberService 객체를 가져다가 자동으로 넣어준다. 이것이 바로 DI(Dependency Injection) 이다.

위 이미지를 보면, memberService는 또 memberRepository를 필요로 한다. 그러면, memberService를 스프링이 생성할때 memberRepository와 연결을 또 시켜 주어야 한다. 그렇게 하려면 MemberService 클래스도 마찬가지로 생성자 위에 @AutoWired 라는 어노테이션을 달아 줘야 한다. 

 

여기까지 했으면 위 이미지와 같이 세개가 모두 성공적으로 연결 된 상태 일것이다.

 

🖇️ 정리

스프링 빈을 등록하는 두가지 방법

  • 컴포넌트 스캔과 자동 의존관계 설정
  • 자바 코드로 직접 스프링 빈 등록하기


컴포넌트 스캔 방식은 위에서 어노테이션을 달아 연결한 방식이다. 즉, 스프링을 처음 시작할때 어노테이션을 스프링이 알아서 찾아 스캔한 후,  어노테이션이 달려있으면 하나의 객체로 만들어 스프링 컨테이너 안에서 독자적으로 관리하는 방식이다.

 

마지막으로 주의할 점은

 

package Jihoo.hello_spring;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class HelloSpringApplication {

	public static void main(String[] args) {
		SpringApplication.run(HelloSpringApplication.class, args);
	}

}

 

 

위 코드는 프로그램의 진입점이다. 이제 스프링이 진입을 할때 모든 태그들을 스캔 한다고 언급했는데, 맨 위에 

package Jihoo.hello_spring의 하위 디렉토리에서만 스캔을 하고 그 이외의 디렉토리는 스캔하지 않는다. 예를 들어, 현재는 

 

src/main/java/Jihoo.hello_spring/HelloSpringApplication 이지만, 예를들어 src/main/java 아래에 'temp'라는 패키지를 생성 후 그 패키지 디렉토리 안에 Demo.java를 만든 후 @Service 태그를 달아 주어도 HelloSpringApplication의 main 함수를 실행해도 이 temp에 있는 @Service 어노테이션은 무시된다.

목차

  • 비즈니스 요구사항 정리
  • 회원 도메인과 저장소 만들기
  • 회원 저장소 테스트 케이스 작성
  • 회원 서비스 개발
  • 회원 서비스 테스트

📎 비즈니스 요구사항

  • 데이터 : 회원  ID, 이름
  • 기능 : 회원 등록, 조회
  • 아직 데이터 저장소가 정해지지 않음(가상의 시나리오)

📎 회원 도메인과 저장소 만들기

 

우선, 현재 진행중인 프로젝트 디렉토리 안에 domain이라는 패키지를 하나 만들어 준다. 이 패키지 안에 사용자 정보를 담을 수 있는 자바 클래스 Member.java를 만들어 준다.

 

여기에는 아래와 같이 필요한 변수를 선언하고 setter과 getter를 추가 해준다.

package Jihoo.hello_spring.domain;

public class Member {

    private long id;
    private String name;

    public Long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }
}

 

참고로 여기서의 id는 회원가입 당시 회원이 정하는 id가 아니라, 데이터베이스에 있는 회원 정보들중 회원을 쉽게 찾아내기 위해 만들어진 id 이다.

 

다음으로, 회원 저장소를 담당할 repository라는 패키지를 하나 더 만들어주고, 그 안에 다음 이미지와 같이 MemberRepository 라는 Interface를 추가 해준다. 

 

이제 MemberRepository 인터페이스에는 아래와 같이 코드를 작성 해준다.

package Jihoo.hello_spring.repository;
import Jihoo.hello_spring.domain.Member;

import java.util.Optional;
import java.util.List;

public interface MemberRepository {

    Member save(Member member);
    Optional<Member> findById(Long id);
    Optional<Member> findByName(Long id);
    List<Member> findAll();
}

 

오랜만이다! 자바의 인터페이스. 처음 자료구조 수업을 들을 때 인터페이스에 대해 헷갈렸었는데, 까먹은 내용을 다시 기억하고자 자바의 인터페이스에 대해 정리해 보고자 한다.

 

자바의 인터페이스(Interface)란,

추상 메서드의 집합을 정의하는 추상 타입이다. MemberRepository 인터페이스를 보면 모든 메서드가 body 없이 선언만 되어 있는 걸 볼 수 있다. 이게 인터페이스의 핵심이다.

인터페이스의 주요 특징은 다음과 같다:

  1. 추상 메서드만 포함
  2. 다중 상속 가능
  3. 느슨한 결합 제공
  4. 구현 클래스를 위한 계약 정의

MemberRepository 인터페이스를 보면 save(), findById(), findByName(), findAll() 메서드가 선언되어 있다. 이 메서드들은 Member 객체를 다루는 기본적인 CRUD 연산을 정의하고 있다.

인터페이스를 사용하면 실제 구현과 명세를 분리할 수 있어서 코드의 유연성과 확장성이 높아진다. 예를 들어, MemberRepository의 실제 구현을 데이터베이스나 파일 시스템 등 다양한 방식으로 할 수 있다. 그래도 인터페이스를 사용하는 코드는 변경할 필요가 없으니 편리하다.

이렇게 인터페이스를 사용하면 코드의 구조를 더 깔끔하게 만들 수 있고, 나중에 유지보수하기도 쉬워진다.

 

이제, 이 인터페이스를 가진 구현체를 만들어 보자.

src/repository 안에 MemoryMemberRepository 라는 자바 클래스를 하나 만들어 준다.

 

package Jihoo.hello_spring.repository;

import Jihoo.hello_spring.domain.Member;

import java.util.*;

public class MemoryMemberRepository implements MemberRepository{

    private static Map<Long, Member> store = new HashMap<>();
    private static long sequence = 0L;

    @Override
    public Member save(Member member) {
        member.setId(++sequence);
        store.put(member.getId(), member);
        return member;
    }

    @Override
    public Optional<Member> findById(Long id) {
        return Optional.ofNullable(store.get(id));
    }

    @Override
    public Optional<Member> findByName(String name) {
        return store.values().stream()
                .filter(member -> member.getName().equals(name))
                .findAny();
    }

    @Override
    public List<Member> findAll() {
        return new ArrayList<>(store.values());
    }
}

 

먼저, 회원들을 저장할 저장소 store를 선언해주고, id를 담을 sequence라는 변수를 선언 해준다.

 

이제, Override한 메서드들을 구현 해줄 차례인데, 먼저

 

- save : 리턴 타입은 Member 객체이고, 입력값으로 받은 member의 아이디를 설정해주고, 저장소에 회원을 담아준다.

 

- findById : findById 메서드는 저장소(store)에서 입력받은 id에 해당하는 Member 객체를 찾아 반환한다. Optional을 사용해 null 처리를 더 안전하게 한다. 이렇게 하면 NullPointerException을 방지하고, 메서드를 호출하는 쪽에서 값의 존재 여부를 쉽게 확인할 수 있다.

 

- findByName : store는 Map 타입의 컬렉션이다. values() 메서드를 호출하면 store에 저장된 모든 값(Member 객체)을 포함하는 Collection을 반환한다. stream() 메서드는 이 Collection을 스트림으로 변환한다. 스트림은 Java 8에서 도입된 기능으로, 컬렉션 데이터를 함수형 스타일로 처리할 수 있게 한다.

 

- findAll : store라는 Map 컬렉션에 저장된 모든 Member 객체를 List로 반환한다.  store는 Map<Long, Member> 타입의 컬렉션 이다. store.values()를 호출하면 store에 저장된 모든 값(Member 객체)을 포함하는 Collection<Member>을 반환한다.
이 Collection<Member>는 Map의 값들만을 포함하는 뷰다. new ArrayList<>(store.values())는 store의 값들을 포함하는 새로운 ArrayList<Member> 객체를 생성한다. 이 과정에서 store.values()로부터 반환된 Collection의 모든 요소가 ArrayList에 복사된다.
이렇게 하면 원본 store의 값들에 영향을 주지 않고, 독립적인 리스트를 반환할 수 있다.

 

자 이렇게, 구현체를 구현했다. 이제 구현체를 작성했으니 이 구현체가 동작하는지 알아보기 위해 테스트 케이스를 작성 해야한다.

 

📎 회원 저장소 테스트 케이스 작성

자바의 가장 유명한 테스트 케이스 프레임워크인 JUnit 이라는 프레임워크를 사용 할것이다. 먼저, src/test/java 하위 폴더에 테스트 케이스를 작성한다. 테스트 케이스들을 쓸 수 있는 디렉토리가 따로 있다. 테스트 케이스는 꼭 여기에다가 작성 해야 한다는것을 잊지 말자!

 

 

 

아래는 작성한 테스트 케이스 코드이다.

package Jihoo.hello_spring.repository;

import Jihoo.hello_spring.domain.Member;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

public class MemoryMemberRepositoryTest {

    MemoryMemberRepository repository = new MemoryMemberRepository();

    @AfterEach
    public void afterEach() {
        repository.clearStore();
    }

    @Test
    public void save() {
        Member member = new Member();
        member.setName("spring");

        repository.save(member);

        Member result = repository.findById(member.getId()).get();
        Assertions.assertEquals(member, result);
    }

    @Test
    public void findByName() {
        Member m1 = new Member();
        m1.setName("spring1");
        repository.save(m1);

        Member m2 = new Member();
        m2.setName("spring2");
        repository.save(m2);

        Member result = repository.findByName("spring1").get();

        Assertions.assertEquals(result, m1);
    }
}

 

- @AfterEach: 각 테스트 메서드가 실행된 후에 실행되는 메서드를 지정한다. 어제 배운 어노테이션이 생각난다.


- afterEach(): 각 테스트가 끝난 후 repository의 저장소를 초기화하는 메서드이다. 이를 통해 각 테스트 간의 데이터를 독립적으로 유지할 수 있다.

 

- @Test: 이 메서드가 테스트 메서드임을 나타낸다.
- save(): MemoryMemberRepository의 save 메서드를 테스트한다.
- Member 객체를 생성하고 이름을 설정한다.
- repository.save(member)를 호출하여 Member 객체를 저장소에 저장한다.
- repository.findById(member.getId()).get()를 호출하여 저장된 Member 객체를 조회한다.
- Assertions.assertEquals(member, result)를 사용하여 저장된 객체가 원래 객체와 동일한지 검증한다.

 

아래는 테스트 케이스를 실행한 후의 콘솔 모습이다. 콘솔에 따로 출력되는 내용은 없으며, 아래 이미지와 같이 케이스 통과시 초록색 체크표시가 뜨면서 테스트를 마무리 한다.

 

 

📎 회원 서비스 개발

이제, 회원 서비스를 구현할 차례이다. 우선, 프로젝트 디렉토리에 새로운 패키지인 'service'라는 패키지를 만들어 준다.

 

 

이렇게 service라는 패키지를 하나 만들고, 그 service라는 패키지 안에 MemberService라는 클래스를 하나 만들어 준다.

 

아래는 MemverService 클래스이다.

 

package Jihoo.hello_spring.service;

import Jihoo.hello_spring.domain.Member;
import Jihoo.hello_spring.repository.MemberRepository;
import Jihoo.hello_spring.repository.MemoryMemberRepository;

import java.util.List;
import java.util.Optional;

public class MemberService {

    private final MemberRepository memberRepository = new MemoryMemberRepository();

    /**
     * 회원 가입
     */
    public Long join(Member member) {
        //같은 이름이 있는 중복 회원 x
        Optional<Member> result = memberRepository.findByName(member.getName());
        result.ifPresent(m -> {
            throw new IllegalStateException("이미 존재하는 회원입니다.");
        });

        memberRepository.save(member);
        return member.getId();
    }

    public List<Member> findMembers() {
        return memberRepository.findAll();
    }

    public Optional<Member> findOne(Long memberId) {
        return memberRepository.findById(memberId);
    }
}

 

회원 서비스는 기본적으로 3가지 기능이 있다.

 

  • 회원 가입 (join)
  • 모든 회원 조회하기(findMembers)
  • 입력값으로 멤버 Id를 던져주면 그 Id에 맞는 회원 1명 찾기(findOne)

코드를 좀 더 살펴보자.

 

- 먼저, 회원가입을 할때, 중복된 이름이 있으면 회원가입이 되지 않도록 하는 코드를 작성해야 한다.

result 라는 변수에 입력값으로 member가 주어지면 저장소에서 그 멤버의 이름을 검색해 이미 같은 이름이 존재한다면(result.ifPresent()), 예외 처리를 하여 회원 가입을 하지 못하도록 한다.

 

만약, 예외가 발생하지 않는다면(중복되는 이름이 없다면), memberRepository에 member를 저장하고, 그 멤버의 멤버 id를 리턴한다.

 

나머지 메서드들은 직관적이라 설명을 생략 하겠다.

 

이런 식으로 회원 서비스를 구현 해주었다. 이제 회원 서비스 로직을 구현했으니, 다음은 이 회원 서비스 로직을 테스트 할 수 있는 테스트 케이스를 작성 해야한다. 아까 언급한 내용처럼 Spring의 테스트 케이스는 모두 src/test 디렉토리에 작성 해주어야 한다.

 

📎 회원 서비스 테스트

아까와 달리 테스트 케이스를 좀 더 편리하게 해주는 단축키가 있다. 테스트를 하려는 클래스(여기서는 MemberService.java)에서 

(Ctrl + Shift + T)를 누르면 아래와 같이 테스트 케이스 클래스를 바로 만들어주는 툴이 등장한다. 이걸로 테스트 케이스를 작성 해보자.

 

package Jihoo.hello_spring.service;

import Jihoo.hello_spring.domain.Member;
import Jihoo.hello_spring.repository.MemoryMemberRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.util.List;
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.*;

class MemberServiceTest {
    MemberService memberService;
    MemoryMemberRepository memberRepository;

    @BeforeEach
    public void beforeEach() {
        memberRepository = new MemoryMemberRepository();
        memberService = new MemberService(memberRepository);
    }

    @AfterEach
    public void afterEach() {
        memberRepository.clearStore();
    }

    @Test
    void join() {
        // given
        Member m = new Member();
        m.setName("hello");

        // when
        Long saveId = memberService.join(m);

        // then
        Member findMember = memberService.findOne(saveId).get();
        assertEquals(m.getName(), findMember.getName());
    }

    @Test
    void findMembers() {
        // given
        Member m1 = new Member();
        m1.setName("member1");
        memberService.join(m1);

        Member m2 = new Member();
        m2.setName("member2");
        memberService.join(m2);

        // when
        List<Member> members = memberService.findMembers();

        // then
        assertEquals(2, members.size());
        assertTrue(members.contains(m1));
        assertTrue(members.contains(m2));
    }

    @Test
    void findOne() {
        // given
        Member m = new Member();
        m.setName("uniqueMember");
        Long saveId = memberService.join(m);

        // when
        Optional<Member> findMember = memberService.findOne(saveId);

        // then
        assertTrue(findMember.isPresent());
        assertEquals(m.getName(), findMember.get().getName());
    }
}

@BeforeEach는 각 테스트 메서드 실행 후에 호출된다. MemoryMemberRepository의 저장소를 초기화하여 테스트 간의 데이터가 격리되도록 한다. 이렇게 추가하면 각 테스트 메서드가 실행될 때마다 새로운 MemberService와 MemoryMemberRepository 인스턴스가 생성되고, 테스트가 끝날 때마다 저장소가 초기화되어 다른 테스트에 영향을 미치지 않도록 한다.




이처럼 오늘은 스프링으로 회원 서비스 로직을 구현 해보았다. 테스트 케이스는 작성되는 디렉토리가 다르고, 테스트 케이스는 build 당시 소스 코드 내부에 포함되지 않는다는 점을 기억하자.

 

 

저번 학기에도 Kotlin으로 안드로이드 앱 개발 수업을 진행하며 수없이 많은 어노테이션을 봤지만 제대로 알고 넘어가지 않았다. 이번에 Spring을 공부하다 보니 생전 처음 보는 어노테이션을 많이 마주했는데, 이번 포스팅에서는 어노테이션이 뭔지 정도만 간략하게 알고 넘어가 보자.

어노테이션(Annotation)이란, 코드에 추가 정보를 제공하기 위해 사용하는 메타데이터의 일종이다. Java와 같은 언어에서 어노테이션은 주석처럼 보이지만, 컴파일러와 런타임 환경에서 특별한 의미를 가진다. 어노테이션은 주로 코드의 가독성을 높이고, 코드의 동작을 변경하거나 추가 기능을 제공하는 데 사용된다.

Spring 프레임워크에서도 많은 어노테이션을 사용하여 다양한 기능을 구현한다. 예를 들어, `@Controller`는 해당 클래스가 컨트롤러 역할을 한다는 것을 나타내며, `@GetMapping`은 특정 URL에 대한 HTTP GET 요청을 처리하는 메서드를 정의하는 데 사용된다. 이러한 어노테이션을 통해 코드의 구조를 명확히 하고, 설정을 간소화할 수 있다.

한 가지 예로, 우리가 작성한 `HelloController` 클래스를 살펴보자:

@Controller
public class HelloController {
    
    @GetMapping("hello-mvc")
    public String helloMvc(@RequestParam("name") String name, Model model) {
        model.addAttribute("name", name);
        return "hello-template";
    }
}


이 클래스에서 사용된 어노테이션을 하나씩 설명해 보겠다.

1. `@Controller`
    - 이 어노테이션은 해당 클래스가 Spring MVC의 컨트롤러임을 나타낸다. 컨트롤러는 사용자의 요청을 받아 적절한 비즈니스 로직을 수행하고, 결과를 뷰에 전달하는 역할을 한다.
    - 컨트롤러 클래스는 보통 웹 애플리케이션의 엔드포인트를 정의하며, 사용자와 상호작용하는 로직을 포함한다.

2. `@GetMapping("hello-mvc")`
    - 이 어노테이션은 `hello-mvc` 경로로 들어오는 HTTP GET 요청을 처리하는 메서드를 정의한다. 즉, 사용자가 `http://localhost:8080/hello-mvc` URL로 요청을 보내면, 이 메서드가 호출된다.
    - `@GetMapping` 외에도 `@PostMapping`, `@PutMapping`, `@DeleteMapping` 등 다양한 HTTP 메서드에 대응하는 어노테이션이 있다.

3. `@RequestParam("name") String name`
    - 이 어노테이션은 요청 파라미터를 메서드의 인자로 매핑하는 데 사용된다. 예를 들어, 사용자가 `http://localhost:8080/hello-mvc?name=spring`와 같이 요청을 보내면, `name` 파라미터의 값인 "spring"이 `name` 변수에 할당된다.
    - 이를 통해 우리는 사용자가 보낸 데이터를 쉽게 받아올 수 있다.

4. `Model model`
    - 이 메서드 파라미터는 뷰에 데이터를 전달하기 위한 모델 객체다. 우리는 `model.addAttribute` 메서드를 사용하여 모델에 데이터를 추가할 수 있다.
    - 이렇게 추가된 데이터는 뷰 템플릿에서 사용되어 동적으로 콘텐츠를 생성할 수 있다.

마지막으로, 이 메서드는 `hello-template`이라는 뷰 이름을 반환한다. Spring은 이 이름을 기반으로 적절한 뷰 템플릿을 찾아 렌더링한다. 여기서 우리가 작성한 `hello-template.html` 파일이 사용된다.

이처럼 Spring에서 어노테이션을 사용하면 코드가 간결해지고, 설정을 위한 XML이나 자바 코드를 많이 줄일 수 있다. 또한, 어노테이션을 통해 코드의 의도를 명확하게 드러낼 수 있어 가독성과 유지보수성이 높아진다.

어노테이션을 잘 활용하면 개발 생산성을 크게 향상시킬 수 있다. 하지만 처음에는 생소할 수 있으므로, 각 어노테이션의 역할과 사용 방법을 하나씩 익혀 나가는 것이 중요하다. 이번 포스팅에서는 어노테이션의 기본적인 개념과 몇 가지 주요 어노테이션의 사용법을 간략히 살펴보았다. 앞으로 Spring을 더 깊이 공부하면서 다양한 어노테이션을 접하게 될 텐데, 그때마다 어노테이션의 의미와 사용법을 잘 이해하고 넘어가는 것이 좋을것 같다!

지난 포스팅에 이어, 오늘은 스프링 웹 개발 기초에 대해 알아 보겠다. 웹을 개발한다는건 크게 보면 3가지가 있다.

 

  • 정적 컨텐츠
  • MVC와 템플릿 엔진
  • API

📍정적 컨텐츠

정적 컨텐츠란 저번에 만들었던 hello 페이지와 같이 아무런 동작이나 상호작용을 하지 않는 말 그대로 '정적'인 컨텐츠이다. 웹페이지 파일을 클라이언트에게 '그대로' 전달 해주는 것이다.

 

먼저, 기본적으로 스프링부트는 정적 컨텐츠를 static이라 불리우는 디렉토리에서 찾아 제공해 준다. 예시를 한번 들어보자.

 

위 이미지처럼 resources/static 디렉토리 안에 hello_static.html이라는 파일을 만들어 주고, 코드는 아래와 같이 작성 해준다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
정적 컨텐츠 입니다.
</body>
</html>

 

IntelliJ에서 서버로 파일을 넘겨주고, 크롬 브라우저를 열어 localhost:8080/hello_static.html을 치면 아래와 같은 화면이 보인다.

 

이처럼 정적 파일을 그대로 클라이언트에게 가져다 준다. 하지만 여기서는 글자만 보이듯이, 우리가 평소에 웹 애플리케이션에서 할 수 있는 다른 활동은 하지 못한다. 말 그대로, 이 정적 웹 페이지에서는 코딩을 해서 다른 기능을 구현 할 순 없다.

 

이제 이 정적 페이지가 어떻게 렌더링 되는지 그림으로 살펴보자.

 

1. 웹 브라우저에 localhost:8080/hello_static.html을 치면, 

 

2. 내장된 Tomcat서버가 요청을 받고, 이제 이 요청을 스프링에게 넘겨준다.

 

3. 이때, 위에 1번이라고 써있는 부분에서는 이전 포스팅에서 언급한 "컨트롤러"가 hello_static.html 파일이 존재하는지 찾아본다.(컨트롤러가 우선순위를 가진다.)

 

4. 하지만, 저번 포스팅을 보면 알겠지만 아래와 같이 hello에 관한 컨트롤러는 있었으나, hello_static에 관한 컨트롤러는 없다.

 

5. 이제 컨트롤러가 이 hello_staic에 관한 컨트롤러가 없는것을 알게 되면, 이제 2번에 쓰여진 resources/static/hello_static.html을 찾는다. 

 

6. 이제 resources/static/hello_static.html 여기에는 이 파일이 존재하는것을 알 수 있다. 이 파일을 찾았으니 다시 클라이언트에게 반환해 준다.

 

정적 컨텐츠는 비교적 간단한 방식으로 동작한다.

 

📍MVC, 템플릿 엔진

MVC는 Model, View, Controller의 약자이다. 저번 학기에 Kotlin으로 모바일 앱을 개발할때 봤던 기억이 난다. 이 방법은 웹서버에서 HTML파일을 조금 더 동적으로 바꿔 클라이언트에게 전달 해주는것을 의미한다.

 

자 그럼, 이번에는 새로운 컨트롤러를 하나 더 만들어 보자. 저번 포스팅에서 이미 만들었던 HelloController라는 자바 클래스에 새로운 컨트롤러를 아래와 같이 만들어 준다.

 

package Jihoo.hello_spring.controller;

import org.springframework.ui.Model;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

@Controller
public class HelloController {

    @GetMapping("hello")
    public String hello(Model model) {
        model.addAttribute("data", "hello!");
        return "hello";
    }

    @GetMapping("hello-mvc")
    public String helloMvc(@RequestParam("name") String name, Model model) {
        model.addAttribute("name", name);
        return "hello-template";
    }

}

 

더보기

@GetMapping("hello-mvc")

- `@GetMapping` 어노테이션은 HTTP GET 요청을 처리하는 메서드를 정의할 때 사용된다.
- `"hello-mvc"`는 이 메서드가 처리할 URL 경로를 지정한다. 즉, `http://localhost:8080/hello-mvc`로 들어오는 GET 요청을 이 메서드가 처리한다.


public String helloMvc(@RequestParam("name") String name, Model model) {


- `public String`은 메서드가 `String` 타입의 값을 반환한다는 것을 의미한다. 이 반환값은 뷰의 이름을 나타낸다.
- `@RequestParam("name") String name`은 요청 파라미터를 메서드의 인자로 받아온다. URL에 `?name=값` 형태로 전달된 파라미터 값을 `name` 변수에 저장한다.
- `Model model`은 뷰에 데이터를 전달하기 위한 인터페이스이다. 이 메서드에서는 모델 객체를 사용하여 뷰에 데이터를 추가한다.

model.addAttribute("name", name);

- `model.addAttribute("name", name);`는 모델에 데이터를 추가하는 코드이다.
- `"name"`은 뷰에서 사용할 데이터의 키값이다.
- `name`은 `@RequestParam`으로부터 전달받은 값이다. 이 값이 뷰에 전달된다.

return "hello-template";

- `return "hello-template";`는 이 메서드가 반환하는 값으로, 뷰의 이름을 나타낸다.
- Spring MVC는 이 반환값을 기반으로 뷰를 찾는다. 예를 들어, 이 경우 `resources/templates/hello-template.html` 파일이 뷰로 사용된다.


이 메서드는 `http://localhost:8080/hello-mvc?name=값` 형태로 들어오는 GET 요청을 처리한다. 요청 URL에서 `name` 파라미터 값을 읽어와서, 이를 `Model` 객체에 추가한 뒤 `hello-template`이라는 이름의 뷰를 반환한다. 뷰 템플릿에서는 모델에 추가된 `name` 값을 사용하여 사용자에게 응답을 생성한다.

이제, 컨트롤러를 추가한 후, resources/templates에 hello-template.html 파일을 하나 만들어 준다.

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Hello MVC</title>
</head>
<body>
<p th:text="'hello ' + ${name}">hello! empty</p>
</body>
</html>

 

여기서 눈여겨볼 부분이 하나 있는데, 

<html xmlns:th="http://www.thymeleaf.org">

 

 

이 부분을 보면, 맨 처음 포스팅때 나왔던 의존성, 라이브러리 설치때 설치했던 thymeleaf라는 라이브러리를 추가해야 한다. 이건 템플릿 엔진으로, 위에서 정적인 html파일을 서빙했던것과 달리, html파일을 조작하여 클라이언트에게 응답해준다.

thymeleaf를 사용하면 html 파일을 동적으로 조작할 수 있다. 즉, 서버에서 데이터를 받아 html 파일에 삽입하거나 html 구조를 변경한 후 클라이언트에게 응답할 수 있다. 이를 통해 더 유연하고 동적인 웹 페이지를 만들 수 있다.

예를 들어, 사용자 정보나 데이터베이스에서 가져온 데이터를 html에 쉽게 삽입할 수 있고, 조건문이나 반복문을 사용하여 html 구조를 동적으로 생성할 수도 있다.

 

이제, IDE에서 실행을 눌러 서버에 파일을 전달 해보자. 전달 후, localhost:8080/hello-mvc.html을 들어가보면...

이런 에러가 뜬다. 에러가 뜨면 항상 로그를 잘 확인해 보자.

 

로그를 확인 해보니 

친절하게도 이렇게 알려줬다. required필드인 name에 아무 값이 없어서 발생한 에러이다.

Mac에서는 Cmd + p를 누르면 이렇게 정보를 볼 수 있는데, boolen required()는 디폴트값이 true이다. 즉, 이 필드는 꼭 무언가를 받아야 하는 필드이며, 받지 않으면 방금처럼 오류가 발생한다.

 

자 그럼, 이제 name파라미터를 넘겨 줘보자.

 

짜잔! ?name=spring! 과 같이 name 파라미터의 값을 넘겨주니 웹 페이지에 방금 넘겨준 spring!이라는 글자가 보인다.

 

🔧 동작 방식은 어떻게 될까?

다시한번 컨트롤러의 코드를 살펴보자.

@GetMapping("hello-mvc")
    public String helloMvc(@RequestParam("name") String name, Model model) {
        model.addAttribute("name", name);
        return "hello-template";
    }

위에서 name=spring!과 같이 파라미터의 값을 넘겨주면, 이 컨트롤러가 먼저 이 파라미터를 받는다. 그러면,

 

- String name부분의 name은 spring!이라는 값이 담길 것이고, 

- 이제 이 컨트롤러에서 name이라는 변수는 내부적으로 spring!이라는 값을 담고있다.

- 이제 또 다른 파라미터로 받은 model에 방금 받은 name객체를 추가 해준다.

 

- 그리고 이제 return hello-template를 통해 hello-template.html로 넘어가면, 

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Hello MVC</title>
</head>
<body>
<p th:text="'hello ' + ${name}">hello! empty</p>
</body>
</html>

여기서 ${name}이 spring!으로 바뀌어 화면에 출력 될 것이다. 여기서 이 달러사인을 보면, 이 달러사인은 모델의 키값을 참조하는데, 이전에 컨트롤러에서 모델에다가 키값이 name인 attribute를 추가해줬으니 여기서 달러사인으로 name을 참조하면, 모델이 담고있는 객체중, 키값이 name인 객체의 값(spring!)을 가져온다.

 

잘 이해가 가지 않으니 아래 그림으로 다시 한번 보자.

 

- 먼저, 브라우저에 localhost:8080/hello-mvc을 입력하면, 내장된 Tomcat서버가 이 요청을 받아 스프링에게 요청을 넘긴다.

 

- 톰캣으로부터 요청을 받은 스프링은, 이 hello-mvc라는 컨트롤러가 helloController.java에 매핑이 되어있는것을 확인하고, 그 메서드를 호출한다.

 

- 이제 호출된 메서드에서 hello-template, name이라는 키값에 spring이라는 값을 가진 모델 객체가 같이 리턴이 되고, 

 

- 이제 이 viewResolver에서 templates/hello-template.html을 찾아 thymeleaf 엔진에게 변환을 요청 후, 클라이언트에게 반환한다.

 

이처럼 아무것도 변환하지 않고 파일 그대로를 보여준 정적 웹 페이지와 달리, 템플릿 엔진을 사용하면 이 html을 변환 후 클라이언트에게 반환 해준다.

 

📍API

이번에도 마찬가지로 HelloController에다가 정적 클래스와 컨트롤러를 하나 추가해준다.

@GetMapping("hello-api")
    @ResponseBody
    public Hello helloApi(@RequestParam("name") String name) {
        Hello hello = new Hello();
        hello.setName(name);
        return hello;
    }

    static class Hello {
        private String name;

        public String getName() {
            return name;
        }

        public void setName(String name){
            this.name = name;
        }
    }

 

그런데 이전에 작성했던 다른 컨트롤러들과 달리 @ResponseBody라는 태그가 추가 되었다. 이는 무엇일까?

- 여기서 말하는 Body는 HTML의 body태그가 아니라, HTTP의 Header, Body를 의미한다. 여기서 이 HTTP의 Body부분에 이 함수에서 return된 "hello " + name 이라는 데이터를 직접 넣어 주겠다는 뜻이다. 이 태그의 기본 반환형은 JSON이다.

 

이제 브라우저를 열어 어떻게 생겼는지 확인 해보자.

 

어떠한 방식으로 이 JSON object가 반환 되었는지 그림으로 살펴보자.

 

- 로컬에서 hello-api로 요청을 보내면, 내장된 톰캣 서버에서 이 요청을 받아 스프링에 전달 해준다.

 

- 그럼 스프링은 hello-api 컨트롤러를 찾게 될것이다. 하지만 여기서 이 컨트롤러에 @ResponseBody라는 태그가 있는것을 확인한다.

 

- 이 @ResponseBody를 확인한 스프링은 HTTP 응답 body에 그대로 이 return : hello(name:spring)을 넘겨야한다고 판단한다.

 

- 하지만 여기서 문제가 있다. 컨트롤러를 보면 알겠지만 위의 MVC 방식과 달리 이번에는 문자열 그 자체가 아닌 객체를 리턴한다. 

 

- 위에서 언급했듯, 이 객체의 타입은 @ResponseBody 태그의 디폴트 타입인 JSON으로 리턴한다.

 

-이때, 이미지에서 보이는 HttpMessageConverter가 동작한다. 기존에는 ViewResolver가 동작했지만, @ResponseBody의 존재가 확실해지면 HttpMessageConverter가 동작한다.

 

- 우리가 넘겨준 hello객체의 모습은 name:spring이고, 키-값을 기반으로 하는 JSON객체와 비슷하다. 이를 인지한 HttpMessageConverter는 JsonConverter를 동작시켜 반환받은 hello객체를 JSON으로 변환시켜 클라이언트에 응답 해준다. 그래서 내가 방금 위에서 처럼 그냥 json객체를 결과로 받은것이다.

 

 

이처럼 스프링부트의 기본적인 3가지 웹 동작 방식을 알아보았다.

 

 

저번 포스팅에서는 자바 스프링의 프로젝트를 생성하는법을 간단하게 배워 보았다. 이번에는 스프링의 라이브러리에 대해 살펴 보겠다.

 

이번에는 View 환경설정에 대해 짚고 넘어 가겠다.

 

이 화면은 저번 포스팅에서 만든 스프링 웹페이지를 localhost:8080에 띄워본 모습이다. 오늘은 저 에러 페이지 대신 정적인 무언가를 띄워보는 실습을 하겠다.

 

 

먼저, src/main/resources/static 아래에 index.html이라는 파일을 만들어 준다.

 

이 파일에는 별 내용이 없다. 그냥 Hello라는 텍스트와, href를 포함한 hello 텍스트가 있는 정적 페이지이다.

 

하지만, 이 정적 html파일은 말 그대로 아무런 동작도 하지 않는 멈춰있는 웹 페이지이며, 웹 서버가 이 html파일을 읽고 클라이언트에게 그냥 전달해주는 정말 간단한 웹 페이지 이다. 이제 정적인 웹 페이지에 다른 기능을 추가 해보자. 

 

 

📃 1. 먼저, src/main/java/Jihoo.hello_spring 아래에 controller라는 패키지를 만들어 준다. 

 

 

🔎 컨트롤러 : 웹 애플리케이션에서 첫번째 진입점을 의미한다. 자바로 생각하면 main 함수 정도라고 생각하면 될것 같다.

 

그 다음, controller패키지 안에 HelloController라는 클래스를 하나 생성해주고, 아래와 같은 코드를 작성한다.

package Jihoo.hello_spring.controller;

import org.springframework.ui.Model;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class HelloController {

    @GetMapping("hello")
    public String hello(Model model) {
        model.addAttribute("data", "hello!");
        return "hello";
    }
}

 

이후, resource/template 아래에 hello.html이라는 파일을 작성하고, 아래와 같은 코드를 html파일에 추가 해준다.

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Hello</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<p th:text="'안녕하세요. ' + ${data}">안녕하세요. 손님</p>
</body>
</html>

 

이제, 웹 브라우저에 localhost:8080/hello를 쳐서 들어가 보면, 

이러한 화면이 떠 있는것을 볼 수 있다.

 

코드를 살펴보면, HelloController클래스에 model.addAttribute("data", "hello!"); 와 같은 코드를 볼 수 있는데, 여기서 data는 키값이고, hello!는 이 키값(data)에 대응하는 value 값이다. 해시 테이블의 키-값 매핑이랑 비슷하다고 생각하면 된다.

 

그리고, 방금 추가한 resource/template 아래에 hello.html파일에서, <p th:text="'안녕하세요. ' + ${data}">안녕하세요. 손님</p> 이 부분을 살펴보면 p태그 안에 텍스트를 넣고, 안녕하세요 + ${data}라고 되어있는것을 볼 수 있다. HelloController파일에서 지정한 키값과 value값이 매핑되어 여기로 치환된다는것을 알 수 있다. 사실 여기까지 하고도 그냥 직관적으로 "다른 파일에 있는 값들이 다른 파일의 동일한 변수와 매핑되는구나" 라고 이해했지만, 좀 더 자세히 이해를 하기 위해 그림으로 알고 넘어가보자!

 

 

 📍Spring의 동작환경

 

-먼저, 웹브라우저에서 localhost:8080/hello라고 던져주면, 이 내장된 Tomcat 웹서버가 이걸 받아 Spring에게 할 일을 위임한다.

 

- @GetMapping("hello") : 여기서의 Get은 HTTP에서 자주 쓰이는 GET, POST, DELETE, PUT과 의미가 같은 GET이다. 이 어노테이션은 Spring Framework에서 사용되며, HTTP GET 요청을 특정 핸들러 메소드에 매핑한다. "hello"는 URL 경로를 나타내며, 이 경로로 들어오는 GET 요청을 해당 메소드가 처리하게 된다. 예를 들어, 이 어노테이션이 붙은 메소드는 'http://yourdomain.com/hello'로 오는 GET 요청을 처리하게 될 것이다. @GetMapping은 @RequestMapping(method = RequestMethod.GET)의 축약형이라고 볼 수 있다.

 

그러면 이제, /hello로 오는 get요청을 처리하기 위해 어노테이션의 아랫 부분인 이 부분이 실행된다.

    public String hello(Model model) {
        model.addAttribute("data", "hello!");
        return "hello";
    }

 

 

- 여기서 return 을 보면 "hello"라는 문자열을 리턴하고 있는데, 기본적으로 스프링부트는 이 return된 hello를 보고, resources/templates의 hello라는 파일을 찾아 렌더링을 한다. 이 메소드는 Spring MVC의 컨트롤러에서 사용되는 핸들러 메소드이다. Model 객체는 뷰에 데이터를 전달하기 위한 컨테이너 역할을 한다. model.addAttribute("data", "hello!")는 "data"라는 키로 "hello!"라는 값을 모델에 추가한다. 이렇게 추가된 데이터는 뷰에서 사용될 수 있다. return "hello"는 뷰 리졸버에 의해 처리되며, 일반적으로 resources/templates 디렉토리에 있는 'hello.html' 또는 'hello.jsp' 같은 템플릿 파일을 찾아 렌더링한다. 이는 스프링부트의 기본 설정이며, 필요에 따라 커스터마이즈할 수 있다.

 

- 컨트롤러에서 리턴값으로 문자를 반환하면 뷰 리졸버(viewResolver)가 화면을 찾아서 처리한다. 처리하는 방식은 다음과 같다. 먼저, 스프링부트 템플릿 엔진에서 기본 viewName을 매핑하는데, 매핑 방식은 resources:templates/ + {ViewName} + .html 과 같이 매핑되어 hello.html을 templates안에서 바로 찾을 수 있는 것이다.

 

참고 : spring-boot-devtools 라이브러리를 추가하면, .html파일을 컴파일만 해주면 서버 재시작 없이 View파일 변경이 가능하다.

 

 📍Build, 실행하기

이제 직접 build하고 실행을 해 보자.

 

내가 위에서 main함수를 실행시켜 정적 웹을 띄운것은 IDE안에서 한 것이고, 이제 IDE밖에서 이 자바 파일을 어떻게 빌드하고 실행 하는지 알아보자.

 

이는 내가 직접 터미널에서 스프링부트 앱을 빌드하고 실행한 모습이다. 

 

-먼저, 프로젝트가 존재하는 디렉토리로 이동 후, ./gradlew build 커맨드를 통해 빌드를 해주고, 이제 build가 완료되면 build된 파일들이 builld디렉토리 안에 존재한다. 이 build 디렉토리로 이동한 후, 또 다시 libs라는 디렉토리로 이동한다.

 

- 이제 여기 build된 자바 파일을 java -jar 커맨드를 이용해 자바 파일을 실행 시켜준다.

 

더보기

빌드 (Build)

빌드는 소스 코드를 컴파일하고, 종속성을 해결하며, 실행 가능한 파일을 생성하는 과정이다. Java 프로젝트의 경우, 빌드는 주로 다음과 같은 작업을 포함한다:

  1. 컴파일: .java 파일을 .class 파일로 변환한다.
  2. 패키징: .class 파일과 다른 리소스 파일들을 하나의 실행 가능한 파일(.jar)로 묶는다.
  3. 테스트: 작성된 테스트 코드를 실행하여 코드가 올바르게 작동하는지 확인한다.
  4. 디플로이: 생성된 실행 파일을 서버나 다른 환경에 배포한다.

빌드 도구인 Gradle, Maven 등을 사용하여 이러한 작업을 자동화할 수 있다. 위의 코드에서는 ./gradlew build 명령어를 사용하여 프로젝트를 빌드하였다.

실행 (Run)

실행은 빌드된 애플리케이션을 실제로 시작하여 작동시키는 과정이다. 실행 파일(.jar)을 자바 런타임 환경에서 실행하여 프로그램이 동작하도록 한다. 위의 코드는 다음과 같은 명령어로 실행하고 있다:

java -jar hello-spring-0.0.1-SNAPSHOT.jar

 

.java 파일과 .jar 파일의 차이점 


.java 파일: 자바 소스 코드 파일이다. 개발자가 작성하는 파일로, 컴파일러에 의해 .class 파일로 변환된다. 이 파일에는 클래스, 메서드, 변수 등의 코드가 포함되어 있다. 예를 들어, HelloController.java 파일은 Spring Boot 애플리케이션의 컨트롤러 역할을 한다.

.jar 파일: Java Archive의 약자로, 여러 .class 파일과 리소스 파일들을 하나의 아카이브 파일로 묶은 것이다. 배포와 실행을 용이하게 하기 위해 사용된다. .jar 파일은 실행 가능한 파일로, java -jar 명령을 통해 실행할 수 있다. 예를 들어, hello-spring-0.0.1-SNAPSHOT.jar 파일은 모든 컴파일된 클래스 파일과 리소스 파일들이 포함된 실행 가능한 파일이다.

 

이렇게 오늘은 

 

1. 템플릿 엔진이 동작하는법, 

2. 정적 페이지를 띄우는법,

3. 프로젝트 빌드, 실행에 대해 알아보았다. 다음 포스팅에서는 더 자세한 내용을 다루도록 하겠다!

 

이제 곧 졸업을 앞두기도 해서 여러 지원 공고를 찾아보니 스프링을 쓰는곳이 많았다. 자바 공화국인 한국만 그럴줄 알았는데, 미국도 다를건 딱히 없었다. 2, 3학년때 자료구조 알고리즘을 자바로 배우며 레쥬메에 자바를 할 수 있다고 썼던 경험이 있는데, 면접을 보니 Spring을 다룰줄 아냐고 묻길래 당황했다. 자바면 자바지 뭔 또 스프링이야? 했는데, 알고 보니 스프링은 미국과 한국 모두에서 매우 널리 사용되는 풀스택 개발 프레임워크로 자리 잡고 있었다... 그래서 이번 기회에 스프링을 공부하려고 한다!

 

먼저, 이 스프링을 공부 할땐 IntelliJ 라는 IDE를 쓸 것을 추천 받았다. 지금까지 VSCode랑 Eclipse만 써온 나로썬 배울것이 하나 더 늘어나 기분이 좋다. 먼저, 프로젝트를 생성하려면 컴퓨터에 Eclipse나 IntelliJ같은 IDE가 깔려 있어야 하고, 그 다음에 Java를 설치 해야한다. 이제는 차례대로 어떻게 프로젝트를 생성하는지 알아 보겠다.

 

📎  프로젝트 생성과 실행

https://start.spring.io/ 여기로 들어가면 스프링 프로젝트를 쉽게 생성 할 수 있는 툴을 제공한다. 

 

먼저, 왼쪽 파티션에는 필요한 프로젝트 상황에 맞춰 여러가지 환경 설정을 해준다.

 

이제, 오른쪽 파티션인 Dependencies를 설정할 차례인데, 이제 내가 진행할 프로젝트는 웹 애플리케이션 개발이기 때문에 아래와 같은 의존성들을 추가 해 주었다.

 

필요한 의존성들을 추가한 후, Genreate버튼을 누르면 .zip파일을 다운 받을 수 있다. 압축을 풀고, 방금 다운받은 IntelliJ IDE에 들어가 방금 다운받고 압축을 해제한 .zip파일을 열어보자.

 

 

짜잔! 생애 처음으로 IntelliJ IDE에서 스프링 프로젝트를 열었다. 생김새가 AndroidStudio랑 똑같이 생겼다. 저번 학기에 Android앱 개발 당시 썼었는데 다시 보니 반갑다!

 

 

프로젝트를 생성하고, 안을 들여다 보면, 현재 highlight된 build.gradle이라는 파일이 보일텐데, 이걸 클릭해보면

 

이렇게 생겨있다. 사실 이 파일은 예전같으면 개발자가 직접 작성하거나, 어디에서 누가 작성해놓은 좋은 코드를 긁어와서 설정해야 했는데, 아까 위에서 언급되었던 https://start.spring.io/ 여기서 프로젝트를 생성한 결과 이러한 설정 파일을 직접 작성하지 않아도 된다. 개발자들의 수고를 덜어주는 노력은 항상 가상하다! 사실 현재 수준에서 이 gradle의 의미 자체를 깊이 파고 들어갈 필요는 없으며, 직관적으로 버전을 설정하고 라이브러리를 가져오는 설정 파일이다 라는 정도로만 이해하면 된다.

 

그냥 넘어가려 했으나 나의 눈길을 잡은 부분이 있었다. 위의 gradle파일의 코드를 확대 해보면 dependency부분에 이런 코드들을 볼 수 있다.

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

 

위에서 내가 dependencies를 설정할때 추가했던 thymeleaf랑 spring-boot-starter-web이 포함이 되어있었다! 그러니까 내가 처음에 https://start.spring.io/ 여기서 의존성을 설정하면 프로젝트를 만들고 gradle파일에 이러한 의존성을 자동으로 설치 해 준다. 또 궁금한점은, 그렇다면 이러한 라이브러리들을 설치하려면 어딘가에서 가져와야 할텐데, 어디서 가져올까? 바로 위에 있는

repositories {
	mavenCentral()
}

 

여기(mavenCentral)서 다운을 받는다는 의미이다. 필요하면 특정 사이트 URL을 넣어주면 된다.

 

여러가지 설정 파일들을 거두절미 하고, 이제 직접 프로젝트를 run해보자.

main - java 안에있는 HelloSpringApplication을 들어가 보면 이러한 모습이다.

 

이제 그냥 이 파일에 있는 main함수를 실행해 보자. 자바를 배워본 사람은 알겠지만, 자바는 ,main()함수를 진입점으로 프로젝트가 실행된다. 이 main함수를 실행하면 콘솔에 포트8080에서 무언가가 실행되었다고 뜨는데, 확인을 위해 크롬 브라우저에 8080을 치고 들어가 보자.

 

이러한 페이지가 뜬다면 성공이다. 위에 favicon은 왜 안뜨는지 모르겠지만 그게 중요한게 아니다. 자바의 스프링부트는 Tomcat이라는 웹서버를 내장하고 있는데, 위 코드처럼 SpringApplication.run을 해주면 자체적으로 웹을 띄우면서 실행하는 방식이다.

 

이렇게 스프링 프로젝트를 공부하기 위해 필요한 자바, IntelliJ, 그리고 https://start.spring.io/ 를 이용해 프로젝트 생성, 실행까지 해 봤는데, 사실 위의 내용들은 간단한 내용이다. 이제 겨우 첫 Spring에 걸음마를 떼었다. 항상 Spring을 쓴다는 내용을 주변에서 듣고, 채용 공고에서도 들어봤지만, MERN 스택으로만 웹 개발을 해온 나로썬 새롭고 설레는 일이다! 다음 포스팅에는 스프링의 라이브러리에 대해 공부해 보겠다.

 

+ Recent posts