유정잉

🎀 스프링 환경 설정, 간단한 회원가입 빌드 작성 본문

개발자 공부/🎀 스프링 공부

🎀 스프링 환경 설정, 간단한 회원가입 빌드 작성

유정♡ 2024. 5. 19. 15:13

[ spring project 생성 ]

 

1. start.spring.io 사이트에서 project 생성

    Gradle - Groovy 선택 버전은 3.0 이상 사용. Group은 package명 Arifact는 project명

    Dependencies는 내가 사용할 라이브러리 선택후 GENERATE

2. 1번에서 생성한 project 파일 압축 풀기 후 -> build.gradle 선택 후 Open

 

3. 요즘에는 project 선택하면 main test 폴더가 자동으로 나눠져서 생성 됨

     test코드가 요즘 개발 트렌드에서 중요함 !!

     resource에는 실제 자바 코드파일을 제외한 설정파일이나 xml이나 properties가 들어가는 폴더 (=즉, 자바파일제외한 나머지)

     build.gradle은 start.spring.io에서 자동으로 제공 된 설정 파일 

     gitignore는 소스코드 관리해주는 파일

 

4. 라이브러리 의존성 확인

* 표시는 중복 제거 한다는 뜻

 


[ View 환경설정 ]

 

1. main/resources/static -> index.html 생성

spring.io 사이트에서 index.html에 대한 설명

2. webserver가 파일을 그대로 던져주는 것이 아닌 탬플릿엔진을 사용하면 내가 원하는대로 루프를 넣거나 모양을 바꿀 수 있음

     => 타임리프 사용 

spring.io 사이트에서 타임리프에 대한 설명

3. resource/templates 폴더에 있는 html 파일을 찾아서 타임리프 템플릿 엔진 처리 

기본적인 동작 환경

4. 타임리프 선언 (.html에서)

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

 


[ 빌드하고 실행하기 ]

콘솔로 이동

1. `./gradlew build`
2. `cd build/libs`
3. java -jar hello-spring-0.0.1-SNAPSHOT.jar

4. 실행확인

 


[ 회원관리예제 만들기 ]

    1. 비즈니스 요구사항 정리

    2. 회원 도메인과 리포지토리 만들기

    3. 회원 리포지토리 테스트 케이스 작성

    4. 회원 서비스 개발

    5. 회원 서비스 테스트 

 

1) 비즈니스 요구사항 정리

   데이터 : 회원 ID, 이름

   기능 : 회원 등록, 조회

   DB는 아직 정해지지 않음

 

2) domain 패키지에 Member 클래스 생성 

package hello.hellospring.domain;

public class Member {

    private Long id; //시스템이 저장한느 id(고객이 정하는 id가 아님)
    private String name;

    public Long getId() {
        return id;
    }

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

    public String getName() {
        return name;
    }

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

 

3) repository 패키지 만들기 (=회원 객체를 저장하는 저장소) -> Interface로 MemberRepository 만들기 

package hello.hellospring.repository;

import hello.hellospring.domain.Member;

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

public interface MemberRepository {
    Member save(Member member); //회원을 저장하면 저장된 회원이 반환
    Optional<Member> findById(Long id); //Id로 회원을 찾기
    Optional<Member> findByName(String name); //Name으로 회원 찾기
    List<Member> findAll(); //지금까지 저장된 모든 회원 리스트 반환

}

 

4) repository 패키지에 MemoryMemberRepository class 생성 후 implements MemberRepository -> implements methods

package hello.hellospring.repository;

import hello.hellospring.domain.Member;

import java.util.*;

//implements MemberRepository 후 🩷 optoion+Enter=Implement methods 생성 됨
@Repository
public class MemoryMemberRepository implements MemberRepository{

    //            회원의 id=Long, 값은 Member save할때 저장될 객체를 Map으로 설정
    private static Map<Long, Member> store = new HashMap<>();
    private static long sequence = 0L; //sequence는 0,1,2 키값을 생성해주는 역할

    @Override
    public Member save(Member member) {
        member.setId(++sequence); //member를 save할때 sequence값 하나 올려줌
        store.put(member.getId(), member);
        return member;

    }

    @Override
    public Optional<Member> findById(Long id) {
        return Optional.ofNullable(store.get(id)); //null이 반환될 가능성이 있으서 Optional로 감싼다
    }

    @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());
    }

    public void clearStore() {
        store.clear();
    }
}

 

5) test에 main에서 만든거랑 똑같이 패키지랑 클래스 생성 이름뒤에만 Test붙이기 MemoryMemberRepositoryTest

     여기서는 MemoryMemberRepository에서 만든 save가 잘 동작하는지 Test 해보는 것 !

    메서드에 @Test 어노테이션 붙여주기만 하면 됨 !

    Test는 순서랑 상관 없이 메서드별로 따로 실행 됨 ! 순서는 알아서 지켜지지 않음 ! -> @AfterEach clearStore 필요 

package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Optional;
import static org.assertj.core.api.Assertions.*;
class MemoryMemberRepositoryTest {

    MemoryMemberRepository repository = new MemoryMemberRepository();

    @AfterEach
    public void afterEach() {
        repository.clearStore(); //Test가 실행되고 끝날때마다 저장소를 비움 !
    }

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

        repository.save(member);

        Member result = repository.findById(member.getId()).get(); //repository.save에 제대로 값 들어갔나 검증
        assertThat(member).isEqualTo(result);
    }

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

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

        Member result = repository.findByName("spring1").get(); //만약에 "spring2"로 바꾸면 오류 !

        assertThat(result).isEqualTo(member1);
    }

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

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

        List<Member> result = repository.findAll();

        assertThat(result.size()).isEqualTo(2);
    }
}

단위 테스트

 

  6) service 패키지 생성 MemberService class 생성 

     ifPresent를 사용하면 이미 존재하는 값이라면 "이미 존재하는 회원입니다" 출력 됨 (=null이 아니면)

     🩷 control + T = Refactor -> Extract Method

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

    memberRepository.save(member);
    return member.getId();
}
//회원가입 + 같은 이름 중복회원 불가능 로직
public long join(Member member) {
	//Optional 생략하고 더 간단하게 코드 작성 가능 -> control + T 로 더 간결하게 가능 
    memberRepository.findByName(member.getName()).ifPresent(m -> { 
        throw new IllegalStateException("이미 존재하는 회원입니다.");
    });

    memberRepository.save(member);
    return member.getId();
}
package hello.hellospring.service;

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

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

@Service
public class MemberService {

    //회원 서비스를 만드려면 회원 리포지토리가 필요
    private final MemberRepository memberRepository;

    @Autowired
    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    //회원가입 + 같은 이름 중복회원 불가능 로직
    public long join(Member member) {
        validateDuplicateMember(member);

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

    private void validateDuplicateMember(Member member) {
        memberRepository.findByName(member.getName()).ifPresent(m -> {
            throw new IllegalStateException("이미 존재하는 회원입니다.");
        });
    }

    //전체 회원 조회
    public List<Member> findMembers() {
        return memberRepository.findAll();
    }

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


}

 

7) 회원 서비스 테스트 MemberServiceTest

   🩷 command + shift + T = 자동 @Test 생성

   given 무언가가 주어졌는데 / when 이거를 실행했을 때 / then 결과가 이게 나와야 함 으로 나눠서 생성 (given/when/then)

package hello.hellospring.service;

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemoryMemberRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

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

// 🩷 command + shift + T = 자동 @Test 생성
class MemberServiceTest {

        MemberService memberService;

        MemoryMemberRepository memberRepository;

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

        @AfterEach //돌때마다 끝나고나면 DB의 값을 날려줌
        public void afterEach() {
            memberRepository.clearStore();
        }

        @Test
        public void 회원가입() throws Exception {
            //Given
            Member member = new Member();
            member.setName("hello");

            //When
            Long saveId = memberService.join(member);

            //Then
            Member findMember = memberService.findOne(saveId).get();
            assertThat(member.getName()).isEqualTo(findMember.getName());
        }
        @Test
        public void 중복_회원_예외() throws Exception {
            //Given
            Member member1 = new Member();
            member1.setName("spring");
            Member member2 = new Member();
            member2.setName("spring");

            //When
            memberService.join(member1);
            IllegalStateException e = assertThrows(IllegalStateException.class,
                    () -> memberService.join(member2));//예외가 발생해야 한다. assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");

        /*
        try {
            memberService.join(member2);
            fail();
        } catch (IllegalStateException e) {
            assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
           }

         */
        }
    }

 

 

8) MemberController class 생성 

    @Component 컴포넌트 스캔과 자동 의존관계 설정 (@Service, @Repository 사용)

package hello.hellospring.controller;


import hello.hellospring.domain.Member;
import hello.hellospring.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

import java.util.List;

@Controller
public class MemberController {

    private final MemberService memberService;

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

}

 

9) config 패키지에 SpringConfig class 생성 -> 자바 코드로 직접 스프링 빈 등록 (@Configuration을 사용하여 직접 @Bean 등록)

     MemberController에 @Controller만 냅두고 @Service랑, @Repository 지우기 ! 

package hello.hellospring;

import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import hello.hellospring.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()); //🩷 command + P = 생성자 뭘 넣어줘야 하는지 보여줌
    }

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

    

10) 이제부터 WEB 화면 웹 MVC 개발 -> HomeController 클래스 생성 -> home.html 생성 

package hello.hellospring.controller;

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

@Controller
public class HomeController {

    @GetMapping("/")
    public String home() {
        return "home";
    }
}
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div class="container">
    <div>
        <h1>Hello Spring</h1> <p>회원 기능</p>
        <p>
            <a href="/members/new">회원 가입</a>
            <a href="/members">회원 목록</a> </p>
    </div>
</div> <!-- /container -->
</body>
</html>

회원가입을 누르면 /members/new & 회원 목록을 누르면 /members

 

11) MemberController에 members/new 와 members Mapping 해주기 ->

     template 패키지에 members패키지 생성 ->  createMemberForm.html 생성

@GetMapping("/members/new")
public String createForm() {
    return "members/createMemberForm";
}

@PostMapping("/members/new")
public String create(MemberForm form) {
    Member member = new Member();
    member.setName(form.getName());
    memberService.join(member);
    return "redirect:/"; //home화면으로 보내는 것
}
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div class="container">
    <form action="/members/new" method="post">
        <div class="form-group">
            <label for="name">이름</label>
            <input type="text" id="name" name="name" placeholder="이름을 입력하세요">
        </div>
        <button type="submit">등록</button> </form>
</div> <!-- /container -->
</body>
</html>

 

12)  MemberForm 클래스 생성 후 String name 필드 선언 & GetterSetter 생성 

       MemberController에 members/new 와 members Mapping 해주기 ->

       template 패키지에 members패키지 생성 ->  memberList.html 생성

package hello.hellospring.controller;

public class MemberForm {
    public String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
    @GetMapping("/members")
    public String list(Model model) {
        List<Member> members = memberService.findMembers();
        model.addAttribute("members", members);
        return "members/memberList";
    }
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div class="container">
    <div>
        <table>
            <thead>
            <tr>
                <th>#</th>
                <th>이름</th>
            </tr>
            </thead>
            <tbody>
            <!-- 타임리프로 로직을 돌면서 이 부분이 반복됨 -->
            <tr th:each="member : ${members}">
                <!-- 첫번째 값을 담고 출력하고 ~ 반복으로 랜더링 됨 -->
                <td th:text="${member.id}"></td>
                <td th:text="${member.name}"></td>
            </tr>
            </tbody>
        </table>
    </div>
</div> <!-- /container -->
</body>
</html>

 

 


@Controller 어노테이션 설정을 하면 스프링 컨테이너에서 스프링 빈이 관리된다 !

스프링 컨테이너에 등록을 하게 되면 객체를 필요할때마다 new로 생성할 필요 없이 하나만 생성해서 여러군데서 사용할수 있음 

즉, private final MemberService memberService = new MemberService(); 대신

     private final MemberService memberService; 사용

같은 스프링빈이면 보통 같은 인스턴스 (=싱글톤), 물론 다르게 설정할 수도 있긴 함 

 

[ 스프링 빈을 등록하는 2가지 방법 ]

1. @Component 컴포넌트 스캔과 자동 의존관계 설정 (@Service, @Repository 사용)

2. 자바 코드로 직접 스프링 빈 등록 (..Config에 @Configuration을 사용하여 직접 @Bean 등록)

 

두가지 방법에 각각 장단점 존재 !

   : 실무에서는 주로 정형화된 컨트롤러, 서비스, 리포지토리 같은 코드는 컴포넌트 스캔을 사용한다.

      그리고 정형화 되지 않거나, 상황에 따라 구현 클래스를 변경해야 하면 설정을 통해 스프링 빈으로 등록한다.

`@Autowired를 통한 DIhelloController, memberService등과 같이 스프링이 관리하는 객체에서만 동작한다.

스프링 빈으로 등록하지 않고 내가 직접 생성한 객체에서는 동작하지 않는다.

 

[ DI 주입 ]

 

필드주입 : 별로 안 좋은 방식 

@Autowired private MemberService memberService;

   

setter주입 : public이라는 단점 노출이 됨 ! 아무 개발자나 호출할 수 있게 열려 있어서 안 좋음 !

private final MemberService memberService;

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

   

생성자 주입 : 제일 좋은 방법 !!! 의존관계가 실행 중에 동적으로 변하는 경우는 거의 아예 없으므로 생성자 주입 권장 !

 

 

728x90

'개발자 공부 > 🎀 스프링 공부' 카테고리의 다른 글

🎀 스프링 기본 1  (0) 2024.05.23
🎀 AOP  (0) 2024.05.22
🎀 JDBC 연결 [ JPA ]  (0) 2024.05.21
🎀 H2 데이터베이스 설치 및 실행  (0) 2024.05.21
🎀 MVC 공부 정리 🎀  (0) 2024.05.18