비즈니스 요구사항 ( 최대한 간단히 개발 )

  1. 데이터: id, name
  2. 기능: 회원가입, 조회
  3. 아직 어떤 db를 쓸 지 정하지 않았음 -> interface로 구현

김영한 강사님 pdf에서 캡쳐

 

 

일반적인 웹 어플리케이션 계층 구조

김영한 강사님 pdf에서 캡쳐

  • 도메인: db에 저장할 객체
  • 리포지토리: db에 접근, 도메인 객체를 db에 저장하고 관리
  • 서비스: 비즈니스 기능 로직(비즈니스 요구사항에서 요구한)
  • 컨트롤러: MVC의 Controller

[main 코드 작성]

구조

 

1. domain/Member

package hello.helloSpring.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;
    }

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

DB에 저장할 멤버를 class로 구현한다.

id는 시스템에서 정해주고 name은 회원가입할 때 사용자가 입력한다.

 

2. repository/MemberRepository, repository/MemoryMemberRepository

//MemberRepository
package hello.helloSpring.repository;

import java.util.List;
import hello.helloSpring.domain.Member;
import java.util.Optional;

public interface MemberRepository {
    Member save(Member member);
    Optional<Member> findById(Long id);
    Optional<Member> findByName(String name);
    List<Member> findAll();
}

MemoryMemberRepository에서 사용할 메서드를 적어준다.

#MemoryMemberRepository
package hello.helloSpring.repository;
import hello.helloSpring.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) {
    	//get을 했을때 null일 수 있어서 Optional로 감싸고 return 한다.
        return Optional.ofNullable(store.get(id));
    }

    @Override
    public Optional<Member> findByName(String name) {
    	//변수 store의 value는 Member 객체이기 때문에 다 돌면서 name과 같은 member가 있는지 확인
        return store.values().stream()
                .filter(member -> member.getName().equals(name))
                .findAny();
    }

    @Override
    public List<Member> findAll() {
    	// store의 모든 member를 List로 반환
        return new ArrayList<>(store.values());
    }

    public void clearStore(){
    	// store를 깨끗이 비운다~
        store.clear();
    }
}

 

 

여기서 꿀팁이 있다면 implements MemberRepository를 쓰고 alt + Enter를 누르고 Implement methods를 누르면

알아서 생성해야할 메서드들의 틀을 잡아준다. 

비어있는 메서드들을 알아서 써준다!!

 

3. service/MemberService

package hello.helloSpring.service;

import hello.helloSpring.domain.Member;
import hello.helloSpring.repository.MemberRepository;

import java.util.*;

public class MemberService {
    private final MemberRepository memberRepository;

    //Dependency Injection
    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    //회원가입
    public Long join(Member member){
        //같은 이름이 있는 중복 회원 X
        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);
    }
}

Dependency Injection이란?

쉽게 말하면

public class MemberService {
    private final MemberRepository memberRepository = new MemoryMemberRepository();
}

선언과 동시에 생성을 해주는게 아니라

public class MemberService {
    private final MemberRepository memberRepository;

    //Dependency Injection
    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}

생성자를 통해 인자로 받아서 넣어주는 방식이다.

MemberService 입장에서는 injection되는 입장이라 Dependency Injection이라고 한다.

 

validateDuplicateMember 메소드에서 드래그한 코드와 안한 코드가 기능적으로는 같다.

하지만 Optional로 바로 뽑아서 사용하는(드래그한 코드) 건 별로 안좋다고 한다.

따라서 체이닝 메서드를 이용하여 코드를 작성했다.


[테스트 코드 작성]

구조

테스트 코드는 service와 repository를 테스트하기 위해 작성하였다.

테스트하기위한 클래스에 Test라는 문자열을 붙여서 이름을 짓는게 일반적이라고 한다.. 절대적인가? 하여튼!!

테스트 코드를 작성할 때, 어떤 상태가 주어지고->given(), 어떤 행동을 할 때->when(), 그러면 이런 결과가 나와야한다-> then() 를 작성하면 된다고 한다. (아래 코드 참조)

 

1. repository/MemoryMemberRepositoryTest

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 static org.assertj.core.api.Assertions.*;

class MemoryMemberRepositoryTest {

    MemoryMemberRepository repository = new MemoryMemberRepository();

    // 각 test를 마치고 항상 호출되는 메서드
    @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(); //get()을 통해 Optional에서 Member를 뽑아냄
        assertThat(member).isEqualTo(result);
        //Assertions.assertEquals(member, result); -> static으로 import 해줘서 윗 줄처럼 사용가능
    }

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

}

 

2. service/MemberServiceTest

package hello.helloSpring.service;

import hello.helloSpring.domain.Member;

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

import hello.helloSpring.repository.MemoryMemberRepository;
import org.junit.jupiter.api.*;


class MemberServiceTest {

    MemberService memberService;
    MemoryMemberRepository memberRepository;

	// 각 test가 시작되기 전 호출되는 메서드
    @BeforeEach
    public void beforeEach() {
        memberRepository = new MemoryMemberRepository();
        memberService = new MemberService(memberRepository);
    }

	//// 각 test를 마치고 항상 호출되는 메서드
    @AfterEach
    public void afterEach() {
        memberRepository.clearStore();
    }

    @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()); //member.getName()대신 member도 가능
    }

    @Test
    public void 중복_회원_예외() { //test코드는 메서드 이름을 한글로 해도 무방하다고 한다!!
        //given(같은 이름을 가진 회원이 존재한다면)
        Member member1 = new Member();
        member1.setName("spring");

        Member member2 = new Member();
        member2.setName("spring");

        //when(join할 때 중복 회원 검증인 validateDuplicateMember할 때 터져야 정상)
        memberService.join(member1);
        // 원래 exception error는 try-catch로 잡는데, 이걸 편하게 해주는게 assertThrows()
//        try {
//            memberService.join(member2);
//        }catch (IllegalStateException e){
//            assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
//        }
        // 에러 메세지 비교를 통한 test
        IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join((member2)));
        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");

        //then

    }

    @Test
    void findMembers() {
    }

    @Test
    void findOne() {
    }
}

 

IntelliJ 꿀팁 단축어

  • alt + insert -> getter, setter, constructor 등 자동 생성
  • ctrl + alt + m -> 메소드 추출하기
  • shift + F6 -> 같은 단어 찾고 바꿀 수 있음

문제

문제

 

처음 작성한 코드

answer = 0
def DFS(idx, numbers, target, value):
    global answer
    if idx == len(numbers):
        if value == target:
            answer+=1
        return

    DFS(idx+1,numbers,target,value+numbers[idx])
    DFS(idx+1,numbers,target,value-numbers[idx])
    
def solution(numbers, target):
    DFS(0,numbers,target,0)
    return answer

1. 탈출조건: 리스트 끝까지 갔을 때

2. 끝까지 갔는데 target이 됐다? 그럼 answer+=1

3. 쭉쭉쭉 더하면서 가다가 리스트 끝까지 다 갔으면 return 하고 다시 빼기로..

 

깨달은 점

1. 문제를 보고 어떤 알고리즘으로 푸는 게 맞는지 생각하는 능력을 길러야겠다..

2. dfs 풀려고 오랜만에 재귀 생각하니까 재밌었다. 쉬워서 그랬나?

3. 재귀를 풀 때는 항상 탈출조건과 어떻게 돌릴건지를 생각하면 된다.

'Programmers' 카테고리의 다른 글

[프로그래머스] 크레인 인형뽑기 게임  (0) 2021.01.07
[프로그래머스] 네트워크  (0) 2021.01.06
[프로그래머스] 다리를 지나는 트럭  (0) 2021.01.05
프린터  (0) 2021.01.03
카펫  (0) 2021.01.03

정적 컨텐츠

client가 요청을 하면 내장 서버인 tomcat이 ~~이런 요청이 들어왔어!라고 하면서 스프링부트 쪽으로 넘겨준다.

그럼 제일 먼저 호출되는 것은 Controller이다. 따라서 url에 get 방식으로 요청이 들어왔을 때, 그와 mapping(@GetMapping)된 함수가 없다면 /static 폴더에서 그에 맞는 파일을 찾아서 넘겨준다.

 

MVC 템플릿 엔진

MVC: Model, View, Controller 

이전 강의에서 배웠듯이, Controller는 Model에 데이터를 담아 View(/templates)에게 쏴준다.

정적 컨텐츠를 쏴줄 때와 다른 점은 Controller가 mapping된 함수를 찾아서 model에 데이터를 넣고 viewResolver에게 이에 맞는 view를 찾아서 변환해줘라~ 라고 한다. 그럼 viewResolver는 템플릿엔진으로 처리하여 변환된 html파일을 client에 반환하여 주는 방식이다.

hello-template.html
view를 통해 반환된 화면의 소스코드

 

API

스프링에서 API 방식은 데이터를 넘겨줄 때 사용한다.

여기서 데이터란 간단한 문자열이 될 수도 있고, 객체가 될 수도 있다. 객체를 넘겨줄 때 기본 형식은 json형식이라고 한다.

이때 annotation은 @ResponseBody이다.

위 코드에서 보면 mapping이 "hello_api"로 되어있으므로 localhost:8080/hello_api(get 방식 요청) 라고 요청했을 때, Controller는 이 함수를 찾을 것이다.

이 때, @RequestParam("name") String name 이라고 해서 파라미터로 name의 값을 받아 name에 넣겠다는 뜻이다.

그럼 요청을 할 때, localhost:8080/hello_api?name=h32j00 라고 하면 name에는 h32j00가 저장된다.

그리고 코드를 보면 return 하는 값이 단순 문자열이 아닌 hello라는 객체임을 확인할 수 있다. 

이 때, Controller가 hello 객체를 보내면 viewResolver가 아닌 HttpMessageConverter(MappingJackson2HttpMessageConverter)가 동작한다. 이 converter가 객체를 json 형식으로 바꿔서 http의 body부분에 넣고 client로 보낸다.

 

@ResponseBody를 사용하여 단순 문자열을 반환했을 때의 소스코드보기 화면
json 형식으로 객체를 넘길 때

 

문제

문제

 

처음 작성한 코드

def solution(bridge_length, weight, truck_weights):
    from collections import deque
    answer = 0
    truck_weights = deque(truck_weights)
    on_bridge = []
    bbb = []
    seconds = [0]*len(truck_weights)
    idx=0
    while True:
        if len(bbb) < bridge_length and sum([i[1] for i in bbb])+truck_weights[0] <=weight:
                a = truck_weights.popleft()
                on_bridge.append([idx, a, True])
                bbb.append([idx,a])
                idx +=1
        if not len(truck_weights):
            answer+= bridge_length+1
            return answer
        for i in on_bridge:
            if i[2]==True:
                seconds[i[0]]+=1
                if seconds[i[0]]==bridge_length:
                    i[2] =False
                    bbb.remove([i[0],i[1]])
        answer+=1

 

코드 리뷰 후

def solution(bridge_length, weight, truck_weights):
    from collections import deque
    answer = 0
    on_bridge = deque([0]*bridge_length) # 큐를 이용하여 다리를 건너는 트럭을 나타낸다. (0으로 채우는 것은 그때의 트럭이 지나가지 않는다는 것)
    truck_weights = deque(truck_weights)
    sum_weight = 0 # sum(on_bridge)할 때 시간 초과가 나기 때문에 sum을 위한 변수
    while truck_weights: # 대기 중인 트럭이 있을 때
        answer+=1 # 1초의 시간
        sum_weight-=on_bridge.popleft() # 다리를 끝까지 지나간 트럭 무게 빼주기
        if sum_weight + truck_weights[0]<= weight: # 문제 상의 조건(대기 중인 트럭이 다리를 건널 수 있는가)
            sum_weight += truck_weights[0] # 다리 위의 트럭 무게를 더한다.
            on_bridge.append(truck_weights.popleft()) # 대기 중인 트럭을 다리 위의 트럭 리스트에 추가
        else: # 다리를 건널 조건이 안되면
            on_bridge.append(0) # 0으로 채운다. (트럭이 못지나가기 때문..)
            
    # bridge_length를 더하는 이유는 대기 중인 마지막 트럭을 빼고 while문을 빠져나오기 때문에 
    # 그 트럭이 다리를 건너는 시간인 bridge_length를 더해주는 것
    return answer+bridge_length 

 

깨달은 점

1. 어떻게 저렇게 생각을 하지?. . . . 난 너무 복잡하게 생각했다.....

2. for문 돌릴 때, ( for i in a: ) 이렇게 해놓고 for 문안에서 a의 원소를 remove하거나 리스트 자체에 변화가 생기면 내 뜻대로 돌아가지 않는다는 사실을 알아냈다..... ㅡ.ㅡ 어려워

'Programmers' 카테고리의 다른 글

[프로그래머스] 네트워크  (0) 2021.01.06
[프로그래머스] 타겟 넘버  (0) 2021.01.05
프린터  (0) 2021.01.03
카펫  (0) 2021.01.03
소수 찾기  (0) 2021.01.03

[2021.01.03] 스프링 공부시작

스프링을 공부해보기 위해 아주 좋은 평의 강의를 찾아냈다.

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%9E%85%EB%AC%B8-%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8

 

스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 - 인프런

스프링 입문자가 예제를 만들어가면서 스프링 웹 애플리케이션 개발 전반을 빠르게 학습할 수 있습니다. 초급 프레임워크 및 라이브러리 웹 개발 서버 개발 Back-End Java Spring MVC Spring Boot 온라인

www.inflearn.com

 

하루에 글 하나를 써보며 그 날 배운 내용을 정리하면서 진행하기로 하였다.

 

프로젝트를 쉽게 만들기 위한 페이지

https://start.spring.io/

 

웹 개발을 하기 위해 최소 필요한 라이브러리

Spring Web과 템플릿 엔진인 Thymeleaf이다.

IntelliJ에서 Gradle이라는 곳을 보면 각 라이브러리의 의존성에 대한 라이브러리들이 쭈르륵 하고 나온다.

 

간단한 동작방식

1. localhost:8080에 접속한다.

spring-boot-starter-web을 땡겨오면 tomcat도 땡겨온다. 즉, 내장서버를 가지고 있다는 사실..

그래서 기본적으로 8080포트를 가지고 접속하면 된다.

 

2. 스프링부트의 welcome page는 /resources/static/index.html 라고 한다.

이 코드의 맞는 정적인 페이지를 보여준다.

3. localhost:8080/hello에 접속한다.

url창에 localhost:8080/hello라고 치면 get 방식으로 통신이 된다.

 

3-1. 내장 톰캣 서버가 hello 아니? 하면서 물어보면

hello라는 문자열에 GetMapping된 클래스를 호출한다. 그때 Model을 만들어서 넘겨주는 데 여기서 Model에 값을 넣어서 return "hello"를 한다.

 

3-2. return "hello"는 이 값을 담은 Model을 hello.html로 보낸다는 의미이다.

 

3-3. hello.html은 /resources/templates/ 아래에 존재한다.

 

3-4. /resources/templates/hello.html

위 코드에서 ${data}라고 받는 부분이 아까 controller에서 보낸 Model에서 attributeName이 'data'인 attributeValue를 뿌려주겠다는 것이다. 즉 ${data}는 hello!!가 되겠지??

 

오늘은 여기까지 하고 내일 다시 공부해야겠다!!

문제

문제

https://programmers.co.kr/learn/courses/30/lessons/42587

 

코딩테스트 연습 - 프린터

일반적인 프린터는 인쇄 요청이 들어온 순서대로 인쇄합니다. 그렇기 때문에 중요한 문서가 나중에 인쇄될 수 있습니다. 이런 문제를 보완하기 위해 중요도가 높은 문서를 먼저 인쇄하는 프린

programmers.co.kr

 

처음 작성한 코드

def solution(priorities, location):
    from collections import deque
    together = deque([f'{chr(65+i)}:{priorities[i]}' for i in range(len(priorities))])
    location = together[location]
    finish = []
    while len(finish)<=len(priorities):
        if len(together):
            a = together.popleft()
        else: 
            finish.append(a)
            break
        for idx,j in enumerate(together):
            if a.split(':')[1] < j.split(':')[1]:
                together.append(a)
                break
            if idx==len(together)-1:
                finish.append(a)
    return finish.index(location)+1

1. 리스트 together에 'A':2, 'B':1 이런식으로 저장한다.

2. 프린트를 하면 리스트 finish에 넣는다.

3. 'A':2 이런식으로 저장했기 때문에 split(':')을 이용하여 비교를 해줘야한다.

4. 포문을 돌면서 마지막까지 돌았는데 나보다 큰 애가 안나오면 finish에 넣는다.

 

코드 리뷰 후

def solution(priorities, location):
    from collections import deque
    together = deque([(i,value) for i,value in enumerate(priorities)])
    cnt = 0
    while True:
        p = together.popleft()
        if any(p[1]<q[1] for q in together):
            together.append(p)
        else:
            cnt +=1
            if p[0]==location:
                return cnt

1. 리스트 together에 (idx,value) 이런 식으로 저장한다. (0,2), (1,1) 이런 식..

2. while문 돌면서 진행하고 내가 원하는 location을 찾으면 반환하고 함수를 종료한다.

3. while문 안에서의 로직은 리스트에서 popleft()로 원소를 뽑고 나보다 큰 게 나오면 조용히 다시 넣고.. 아니면 출력하고(cnt+=1) 그 출력한 것이 내가 원했던 location이라면 출력한 순서인 cnt를 반환하고 종료한다.

 

깨달은 점

1. together에 'A':2 이런식으로 넣다보니 코드가 조금 지저분해졌다.. index를 활용하자!

2. 출력할 때마다 cnt를 +1 하고 그 때 location이랑 비교하는 건 사실 생각치도 못했다.. ㅜ.ㅜ

3. 너무 어렵게 생각했다..

4. collections의 deque 좋다.. popleft() 좋다

'Programmers' 카테고리의 다른 글

[프로그래머스] 타겟 넘버  (0) 2021.01.05
[프로그래머스] 다리를 지나는 트럭  (0) 2021.01.05
카펫  (0) 2021.01.03
소수 찾기  (0) 2021.01.03
조이스틱  (0) 2021.01.02

문제

문제

 

처음 작성한 코드

def solution(brown, yellow):
    s = brown+yellow
    for i in range(s,0,-1):
        if s%i==0:
            if (i-2)*(s/i-2)==yellow:
                return [i,s//i]

가로(i)가 세로(s//i)보다 크거나 같다고 해서 for문돌때 0이 아닌 s부터 시작해서 다른 조건문없이 구할 수 있게 하였다. 

 

깨달은 점

1. 완전탐색 문제 중에 젤 빨리 푼 것 같다 히히  레벨2가 맞나????

'Programmers' 카테고리의 다른 글

[프로그래머스] 다리를 지나는 트럭  (0) 2021.01.05
프린터  (0) 2021.01.03
소수 찾기  (0) 2021.01.03
조이스틱  (0) 2021.01.02
기능개발  (0) 2021.01.02

문제

문제

 

처음 작성한 코드 // 시간 초과 (코드 없어짐..)

 

코드 리뷰 후

def is_prime(n):
    if n<2: return False
    for i in range(2, int(n**0.5)+1):
        if n%i==0:
            return False
    else: return True

def solution(numbers):
    from itertools import permutations
    answer = 0
    result = []
    for i in range(len(numbers)):
        result += set(map(int, map(''.join, permutations(numbers, i+1))))
    for i in set(result):
        if is_prime(i): answer+=1
    return answer

 

깨달은 점

1. 모든 경우의 수를 다 따지는 거라면 permutations가 내가 짠 것보다 빠르다..

2. 소수를 찾을 때에는 최대 약수가 sqrt(n) 이하이므로 거기까지만 체크하면 된다.

 -> 주어진 자연수 N이 소수이기 위한 필요충분 조건은 N이 N의 제곱근보다 크지 않은 어떤 소수로도 나눠지지 않는다. 

'Programmers' 카테고리의 다른 글

프린터  (0) 2021.01.03
카펫  (0) 2021.01.03
조이스틱  (0) 2021.01.02
기능개발  (0) 2021.01.02
주식가격  (0) 2021.01.01

문제

문제

 

처음 작성한 코드 // 90.9 점 나옴

def solution(name):
    alphabet = [chr(65+i) for i in range(26)]
    result = ['A']*len(name)
    answer = 0
    cursor = 0
    for idx,value in enumerate(name):
        i = alphabet.index(value)
        if i < 26-i:
            answer += i
        else:
            answer += (26-i)
        if idx+1!= len(name) and result[idx+1] != name[idx+1]:
            if idx+1-cursor < cursor+1: # 오른쪽으로
                answer+= (idx+1-cursor)
            else: # 왼쪽으로 돌아서 맨 오른쪽으로
                answer += cursor+1
            cursor = idx+1 # 'A' 인 애들 뛰어넘기
    return answer

 

문제 링크

https://programmers.co.kr/learn/courses/30/lessons/42860#

 

코딩테스트 연습 - 조이스틱

조이스틱으로 알파벳 이름을 완성하세요. 맨 처음엔 A로만 이루어져 있습니다. ex) 완성해야 하는 이름이 세 글자면 AAA, 네 글자면 AAAA 조이스틱을 각 방향으로 움직이면 아래와 같습니다. ▲ - 다

programmers.co.kr

 

깨달은 점

문제에서 "최소값"을 찾으라 하여 찾았는데.. 90.9점이 뜹니다.. 제 코드를 보시고 혹시 잘못된 점이 있다면 댓글에 남겨주시면 감사하겠습니다!!

'Programmers' 카테고리의 다른 글

카펫  (0) 2021.01.03
소수 찾기  (0) 2021.01.03
기능개발  (0) 2021.01.02
주식가격  (0) 2021.01.01
이상한 문자 만들기  (0) 2021.01.01