ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 스프링입문 - 백엔드 개발, 빈과 의존관계, MVC패턴
    스프링/스프링부트+JPA - 블로그 2021. 8. 26. 11:08

    인프런 '김영한'님의 강의 참조!

     

     

    - 회원 관리 예제(백엔드 개발)

     

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

    • 쓰는 데이터 딱 2가지 : 회원ID, 이름
    • 기능: 회원 등록, 조회
    • 데이터 저장소가 정해지지 않은 상태에서 개발한다는 가상의 시나리오

    스프링 MVC패턴의 핵심중의 핵심 계층 구조이다!

    1. 컨트롤러 - MVC패턴의 중심에서 컨트롤 역할
    2. 서비스 - 핵심 비지니스 로직 구현(회원중복가입안됨 등)
    3. 리포지토리 - DB에 접근하여 도메인 객체 그 자체를 저장하거나 불러오는 역할
    4. 도메인 - 비지니스 로직에 쓰이는 객체(회원,주문,쿠폰 등 DB에 저장하고 관리되는 부분)

     

     

     

     

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

    • DB 플래폼이 정해지지않았다는 가정하에 리포지토리를 INTERFACE화 하여 나중에 선정되면 바꿀 수 있도록 하기 위함
    • 멤버리포지토리를 인터페이스화하여 현재 기능을 자유자재로 쓰도록 구성 함
    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;
    	}
    	public void setName(String name) {
    		this.name = name;
    	}
    }
    public interface MemberRepository {
    	Member save(Member member); //회원 저장 기능
    	Optional<Member> findById(Long id); //Optional은 null 반환 기능
    	Optional<Member> findByName(String name);
    	List<Member> findAll(); // 모든 회원 찾기
    }
    • 인터페이스를 받아서 기능 세팅
    public class MemoryMemberRepository implements MemberRepository {
    
    	private static Map<Long, Member> store = new HashMap<Long, Member>();
    	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<Member>(store.values());
    	}
    }
    • 현재 완벽한 MVC패턴을 구현한게 아니기때문에 기능만 보기 위한 간단히 세팅을 한 것
    • Map이 가지고 있는 특성을 이용해서 아이디와 이름을 찾는 세팅이다
    1. save에서는 interface에서 구현한것을 기반으로 아이디가 세팅되면 시퀀스가 올라가도록하고 map을 통해 그 아이디를 저장한다
    2. findByIdsms optional을 통해 null을 처리하고 map에 저장된 아이디를 get으로 찾아온다
    3. findByName은 map과 람다식을 이용하였고 필터를 통해 getName시에 name과 같은것을 하나라도 있으면 찾아온다
    4. findAll은 모두 찾는것이기때문에 list형식으로 store의 키값에 해당하는 모든것을 찾음

     

    stream()관련 글은 밑에 블로그 참조!!!

    https://jeong-pro.tistory.com/165

     

    자바 스트림(Stream) API 정리, 스트림을 이용한 가독성 좋은 코드 만들기(feat. 자바 람다, 함수형 프

    Java Stream 자바 공부를 하면서 Stream이 무엇인지, 어떻게 사용되고 있는지 인지는 하고 있었으나 실제 코드로 타이핑해보지 않았다. 그러던 중 이번에 가볍게 API 훑어보는 식으로 공부를 하면서

    jeong-pro.tistory.com

     

     

     

     

     

    3) 테스트 케이스 작성

    • 지금까지 만든것이 정상적으로 작동하는지 보는 것
    • 보통 Junit을 통해서 한다
    public class MemoryMemberRepositoryTest {
    
    	MemberRepository repository = new MemoryMemberRepository();
    	
    	//세팅된 이름값과 findById값이 같은지 테스트 
    	@Test
    	public void save() {
    		Member member = new Member();
    		member.setName("Vic");
    		repository.save(member);
    		
    		Member result = repository.findById(member.getId()).get();
    //		Assertions.assertEquals(member, result);
    		assertThat(member).isEqualTo(result);
    	}

    정상!

    public class MemoryMemberRepositoryTest {
    
        MemberRepository repository = new MemoryMemberRepository();
        ....
    
        @Test
        public void findByName() {
          Member member1 = new Member();
          member1.setName("vic");
          repository.save(member1);
    
          Member member2 = new Member();
          member2.setName("cha");
          repository.save(member2);
    
          Member result = repository.findByName("cha").get();
    
          assertThat(result).isEqualTo(member1);
    }

    정상 작동이 아니면 빨간불!

    public class MemoryMemberRepositoryTest {
    
    ...
    
    	@Test
    	public void findAll() {
    		Member member1 = new Member();
    		member1.setName("vic");
    		repository.save(member1);
    		
    		Member member2 = new Member();
    		member2.setName("cha");
    		repository.save(member2);
    		
    		List<Member> result = repository.findAll();
    		
    		assertThat(result.size()).isEqualTo(2);
    	}

    정상!

     

    • Junit 테스트시 주의사항은 한번에 한클래스의 여러 메소드를 작동하면 메소드 작동 순서가 보장이 안된다. 그래서 오류가 발생 할 수도 있다. 그래서 반드시 매 매소드마다 클리어를 시켜줘야한다!
    public class MemoryMemberRepository implements MemberRepository {
    ...
    	public void clearStore() {
    		store.clear();
    	}
    }
    public class MemoryMemberRepositoryTest {
    
    	MemoryMemberRepository repository = new MemoryMemberRepository();
    	
    	@AfterEach
    	public void afterEach() {
    		repository.clearStore();
    	}
        ...
    • 원래 MemberRepository객체를 테스트가 필요한 메모리로 바꾸고 거기에 clear() 시켜주는 메소드를 만든 후 @AfterEach의 기능을 이용해서 각 메소드 작동 후 비우도록 설정 한 것. 정말 중요하다!

     

     

     

    4) 회원 서비스 개발

    • 서비스는 항상 비지니스와 연관이 되도록 이름들을 세팅하는게 좋다!
    public class MemberService {
    
    	private final MemberRepository memberRepository = new MemoryMemberRepository();
    
    	//회원가입
    	public Long join(Member member) {
    		//중복체크
    		validateDuplicateMember(member);
    		memberRepository.save(member);
    		return member.getId();
    	}
    
    	private void validateDuplicateMember(Member member) {
    		memberRepository.findByName(member.getName()) 
    		// Optional 멤버이기때문에 Optional이 들고 있는 기능 사용 가능
    		.ifPresent(m -> {
    			throw new IllegalStateException("The ID already exist");
    		});
    	}
    	
    	//회원조회
    	public List<Member> findMembers() {
    		return memberRepository.findAll();
    	}
    	
    	public Optional<Member> findOne(Long memberId){
    		return memberRepository.findById(memberId);
    	}
    }

     

    4-1) 회원서비스 테스트

    class MemberServiceTest {
    	
    	MemberService memberService = new MemberService();
    	
    	@Test
    	void Join() {
    		//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 IdException() {
    		//given
    		Member member1 = new Member();
    		member1.setName("spruing");
    		
    		Member member2 = new Member();
    		member2.setName("spruing");
    		
    		//when
    		memberService.join(member1);
    		IllegalStateException e =  assertThrows(IllegalStateException.class, () -> memberService.join(member2));
    		assertThat(e.getMessage()).isEqualTo("The ID already exist");
    //		try {
    //			memberService.join(member2);
    //			fail();
    //		} catch (IllegalStateException e) {
    //			assertThat(e.getMessage()).isEqualTo("The ID already exist");
    //		}
    		//then
    	}

    정상

    • 똑같이 전체 한번에 테스트를 위한 clear() 세팅
    class MemberServiceTest {
    ..
    	MemoryMemberRepository MemberRepository = new MemoryMemberRepository();
        
        	@AfterEach
    	public void afterEach() {
    		memberRepository.clearStore();
    	}
        ..
        }
    }

     

     

     

     

    *지금까지 테스트시에 new로 인스턴스한 객체와 실제로 테스트가아닌 자바패키지 안에서 만들어진 객체가 다른 문제가 있다. (예:MemberService에서 new한 MemoryMemeberRepository와 테스트에서 선언한 MemoryMemeberRepository가서로 다른 객체이다) 반드시 테스트와 자바패키지안의 내용물은 같은걸로 써야한다. 이를 해결해보자! 

    public class MemberService {
    	private final MemberRepository memberRepository = new MemoryMemberRepository();
        
        
    --> 위에의 것을 생성자로 바꿔서 객체화를 통일시킨다!
    
    public class MemberService {
            private final MemberRepository memberRepository;
            public MemberService(MemberRepository memberRepository) {
                this.memberRepository = memberRepository;
            }
    class MemberServiceTest {
    	
    	MemberService memberService;
    	MemoryMemberRepository memberRepository;
    	
    	@BeforeEach
    	public void beforeEach() {
    		memberRepository = new MemoryMemberRepository();
    		memberService = new MemberService(memberRepository);
    	}
        ...
    • 테스트를 실행하게되면 제일먼저 @BeforeEach가 실행되고 memberRepository 객체가 만들어지고 그 객체가 memberService생성자로 들어가게된다. 그렇게되면 같은 데이터를 쓰는 것, 즉 DI의 관계에 엮이는 것!

     

     

    현재까지 한 것 간단 정리

    1. 도메인(Member.java) - 비지니스 로직에 쓰이는 전체 객체(아이디/이름)
    2. 레포지토리(MemberRepository.java(Interface)) - 인터페이스로 정의 후 기능 강제 및 어디서든 다양하게 쓰도록 함
      MemoryMemberRepository.java - 인터페이스를 받아서 DB에 실제로 접근하여 데이터 송수신, 데이터를 저장하거나 꺼내오거나 등등
      (아이디저장,아이디조회,전체조회 등)
    3. 서비스(MemberService) -  레포지토리를 통해 세팅된 DB접근용 코드로 실제 비지니스 기능 구현
      (회원가입,회원조회등)

     

     

     

     

     

     

     

    5) 스프링 빈과 의존관계

    • 이제 만들어놓은 기능들을 화면에 나타내고 싶다. 컨트롤러와 템플릿엔진이 필요하다
    • 템플릿은 화면을 그린다면 컨트롤러는 서비스를 통해서 기능을 구현하고 데이터를 주고받는데 이를 의존관계가 있다고 한다
    • @Controller 어노테이션을 통해 빈이 생성되고 컨테이너로 관리가 된다. 즉 new로해서 만들지 않는다. 스프링이 관리하도록 한다
    @Controller
    public class MemberController {
    	private final MemberService memberService;
    	@Autowired
    	public MemberController(MemberService memberService) {
    		this.memberService = memberService;
    	}
    }
    • 서비스,리포지토리 또한 어노테이션으로 빈처리를 하자
    @Service
    public class MemberService {
    
    
    @Repository
    public class MemoryMemberRepository implements MemberRepository {

    • 사진에서 설명하듯이 컨트롤러는 서비스가 필요하고 서비스는 레포지토리가 필요하게되는 서로 의존관계에 있다. 그 의존관계를 이어주는 @Autowired! 그렇게되면 서로 작동할때 반드시 엮이게 되는 장점이있다
    • 생성자, 멤버변수, 필드에 선언이 가능하다
    	@Autowired
    	public MemberService(MemberRepository memberRepository) {
    		this.memberRepository = memberRepository;
    	}
        
        	@Autowired
    	public MemberController(MemberService memberService) {
    		this.memberService = memberService;
    	}
    • 더 많은 지식은 아래 링크를 참조하자!

    https://devlog-wjdrbs96.tistory.com/166

     

    [Spring] @Autowired란 무엇인가?

    저번 글에서 IoC 컨테이너와 빈(Bean)등록에 대해서 정리해보았다. 다시 정리하자면 의존성 주입과 빈 등록은 다른 것인데 일단 IoC 컨테이너에 빈으로 등록이 되어야 의존성 주입을 할 수 있다. 저

    devlog-wjdrbs96.tistory.com

     

     

     

     

    6) 자바 코드로 직접 스프링 빈 등록하기

    @Configuration
    public class SpringConfig {
    	
    	@Bean
    	public MemberService memberService() {
    		return new MemberService(memberRepository());
    	}
    	
    	@Bean
    	public MemberRepository memberRepository() {
    		return new MemoryMemberRepository();
    	}
    }
    • 직접 등록을 하게되면 등록시에 자기 입맛대로 설정을 바꿀 수 있다는 장점이 있다.

     

     

     

    7) MVC 간단 패턴으로 완료하기

    - 홈 화면 추가

    @Controller
    public class HomeController {
    
    	@GetMapping("/")
    	public String home() {
    		return "home";
    	}
    }
    *home.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>
    <div class="container">
    	<div>
    		<h1>Hello Spring</h1>
    		<p>회원 기능</p>
    		<p>
    			<a href="/members/new">회원 가입</a>
    			<a href="/members">회원 목록</a>
    		</p>
    	</div>
    </div>
    </body>
    </html>

    항상 스캔되는 첫 부분은 스프링 컨테이너이다 static 이하가 아니다

     

    - 회원 등록

    @Controller
    public class MemberController {
    ...
    	@GetMapping("/members/new")
    	public String createForm() {
    		return "members/createMemberForm";
    	}
    }
    *createMemberFrom.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>
    <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>
    </body>
    </html>

    • 데이터를 받을 폼 생성
    public class MemberForm {
    	private String name;
    
    	public String getName() {
    		return name;
    	}
    
    	public void setName(String name) {
    		this.name = name;
    	}
    	
    }

    --> html에서의 name 값과 매칭 시키는 것

    • 직접 데이터를 들고 컨트롤이 수행되는 @PostMapping
    @Controller
    public class MemberController {
    ...
    	@PostMapping("/members/new")
    	public String create(MemberForm form) {
    		Member member = new Member();
    		member.setName(form.getName());
    		
    		System.out.println("member.setName = " + member.getName());
    		
    		memberService.join(member);
    		
    		return "redirect:/";
    	}
    	
    }
    • GetMapping은 단순 이동, PostMapping은 무언가를 들고 이동
    • 데이터를 받았으니 그것을 기반으로 create()가 실행된다. Member 클래스가 객체화 되고 이 객체에 쓰일 name을 MemberForm을 통해 받는다. 그 후 서비스에 저장(join)한다.

    • 받아오는 데이터가 잘 나온다

     

    - 회원 조회

    	@GetMapping("/members")
    	public String list(Model model) {
    		List<Member> members = memberService.findMembers();
    		model.addAttribute("members", members);
    		return "members/memberList";
    	}
    *memberList.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>
    <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>
    </body>
    </html>
    • 컨트롤러에서 단순 이동을 하는데 그 이동시에 리스트 형태로 모든것을 담는다
    • 리스트 형태로 담긴 members를 .addAttribute로 키와 밸류로 저장한다 
    • 그리고 html에서 타임리프 태그를 통해 member의 밸류값을 불러오는것

     

Designed by Tistory.