주제 : 모든 API에 로그인 기능을 적용시키기

 

Why?

  • 연구실 데이터를 CRUD하는 API 인데 아무나 접근할 수 있으면 보안상 문제가 많다고 생각
  • 데이터를 사용하는 연구원들도 Read 이외 생성, 수정, 삭제 기능에 접근 제한을 두는 것이 데이터 관리에 있어서 중요하다고 생각

-> 데이터를 사용하는 연구원들은 Read 기능만 접근가능하게 함

-> 디비 관리자만 모든 기능 접근 가능하게 함

 

How?

  • 모든 연구원들이 회원가입하여 아이디 비번을 생성하고 관리자가 슈퍼user, 일반user를 지정해줘도 되지만 개인 유저가 가지는 큰 의미가 없어 슈퍼user와 일반user 두개의 아이디만 필요하다고 생각 -> 나중에 회원가입 기능이 추가되고 개개인이 갖는 위치데이터 입력시 개인 id를 갖는게 더 유리할 수 있음
  • 쿠키, 세션, JWT 중 JWT 사용
    • 쿠키는 쿠키값 그대로 보내 보안의 문제
    • 세션은 서버에 추가적인 저장소가 필요함
    • 토큰방식은 디비 저장소도 필요없고 로그인시 암호화된 문자열을 보내 보안상 안전함
    • 좀 더 고민해보자
  • 서브모듈 적용하여 application.yml 파일에 있는 보안 정보(디비 정보, jwt secretKey 등) 숨김

 

진행

User Table

  • is_super_user 필드로 슈퍼user와 일반user 구분

 

JWT

  • TokenProvider
    • jwt 생성하고 추출하는 기능
  • LoginInterceptor
    • 컨트롤러에 닿기전에 가로채 로그인이 되어 있는지(제대로된 토큰이 넘어왔는지) 확인하는 기능
  • AuthArgumentResolver
    • 인터셉터 작업이 끝난 뒤 컨트롤러로 갔을때 필요한 토큰 값을 원하는 형태로 받고 싶을때 사용
      • Authentication 어노테이션 → 컨트롤러 아규먼트 어노테이션
      • CurrentUser 클래스 → 아규먼트를 담당하는 클래스
  • WebMvcConfig
    • LoginInterceptor 와 AuthArgumentResolver 를 Bean 으로 등록하고
    • 각각 registry 와 resolvers 에 등록한다

 

서브모듈

  • 보안 정보를 담을 private repository 만들고
  • 본 프로젝트의 서브모듈로 설정함
  • processResources.dependsOn('~~~') 를 사용하여 원하는 위치에 파일을 복사하여 옮김
  • application.yml 에 있는 데이터는 @Value 어노테이션을 사용하여 가져올 수 있다

 

결과

import requests
import json

headers = {'Content-Type': 'application/json; chearset=utf-8'}
data = {'name': '일반유저', 'password': ****}
res = requests.post('http://localhost:8080/login/oauth', data=json.dumps(data), headers=headers)
headers = {'Content-Type': 'application/json; chearset=utf-8', 'Authorization': 'Bearer '+res.json()['accessToken']}

res = requests.get('http://localhost:8080/mobility/getData?offset=0&limit=10&name=omg', headers=headers)
print(str(res.status_code) + " | " + res.text)
  • 연구원들이 주로 파이썬 언어를 사용하기 때문에 파이썬 코드에서 데이터를 가져오는 코드
  • 로그인이 되어 있지 않으면 데이터를 받아올 수 없다

고민

  • 프론트엔드에서 헤더를 설정하는데 어떻게 할까
  • 코드에서 데이터를 받아오는게 나을까? 웹페이지에서 데이터를 다운받아서 쓰는게 나을까? 직접 디비에 연결해서 사용하는게 나을까?

우아한테크코스에서 진행하는 미션을 혼자 진행해보면서 나의 생각과 배운 것을 정리하는 글

[페어매칭 미션]

[내코드PR]

 

[페어매칭] 미션 제출 by changjun6518 · Pull Request #1 · changjun6518/java-pairmatching-precourse

테스트 코드 작성 MVC 패턴 객체 메세지 전달 의존관계

github.com

 


Course enum에 여러 변수들?을 담았는데 책임이 많아져서 문제?

public enum Course {
    BACK_END("백엔드", "./src/main/resources/backend-crew.md"),
    FRONT_END("프론트", "./src/main/resources/frontend-crew.md"),
    ;

    static{
        for (Course course : Course.values()) {
            course.initCrews();
        }
    }

    private String name;
    private String filePath;
    private Crews crews;

    Course(String name, String filePath) {
        this.name = name;
        this.filePath = filePath;
    }

    private void initCrews() {
        crews = CrewFileReader.readCrews(filePath);
    }
}
  • Course 이름 , filePath, 크루 리스트 등 과정에 속해있는 필요한 요소들을 enum안으로 다 담았는데 적절한 방법으로 사용한 건지 모르겠다
  • static은 프로그램을 실행하자마자 데이터를 보유하고 있어하게끔 (캐쉬 역할을 하게) 함

Enum의 예외처리는 어떻게할까?

  • 이건 검증로직이 어디에 있을까에 대한 의문인 것 같은데 enum도 클래스처럼 안에 하는게 좋겠지?

백엔드 과정 및 프론트 과정 크루 리스트 받아올때 중복 코드 어떻게 제거함?

public class CrewFileReader {
    static List<String> crewsString = new ArrayList<>();

    public static Crews readCrews(String filePath) {
        try {
            BufferedReader bufferedReader = new BufferedReader(
                    new FileReader(filePath)
            );
            String str;
            Crews crews = new Crews();
            while ((str = bufferedReader.readLine()) != null) {
                crewsString.add(str);
                crews.add(str);
            }
            bufferedReader.close();
            return crews;
        } catch (Exception e) {
            throw new IllegalArgumentException("제대로 파일을 읽어올 수 없습니다!");
        }
    }
}
  • 인터페이스로 처리해보고 싶은데 잘 모르겠음
  • 매개변수로 파일경로 주입시켜줬음 -> 파일경로를 설정해주는 역할을 상위 객체로 넘김

이미 매칭되었는지 판단할때 map으로 확인하는데 String 값의 경우 객체기 때문에 포인터가 다를 것 같은데 contains를 사용했을때 같은 객체라고 판단할까?

private final HashMap<Level, List<String>> crewsByLevel;

public boolean isMatched(Level level, String crewName) {
    if (crewsByLevel.containsKey(level)) {
        List<String> matchedCrews = crewsByLevel.get(level);
        return matchedCrews.contains(crewName);
    }
    return true;
}
  • Heap 에 존재하는 객체들이기 때문에 같은 객체라고 판단할 것이다
  • 여러 군데에 쓰여도 같은 객체를 가리키고 있다
  • Key가 String이라면 equals 함수의 내부 로직이 있을거야...! 아마도

Matching - Crews - Crew 페어 매칭 이력 검사해서 이미 매칭되었는지 검증하는 함수 명을 똑같이 하는게 맞는가?

  • 3개 클래스 모두 매칭되었는지 검증하는 함수명이 isMatched인데 헷갈린다
  • 근데 의존관계?가 명확하면 그렇게 헷갈리지 않을 것 같기도함 -> 찾아보자

Matching 클래스를 불변객체? 로 두고 하나의 객체만 계속 사용하도록 만들면 공간효율이 좋지 않을까 생각함

public class Matching {

    private final List<String> stringCrews;

    public Matching(List<String> stringCrews) {
        this.stringCrews = stringCrews;
    }

    public MatchingResult match(Level level, Crews crews) {
        shuffle();
        MatchingResult matchingResult = new MatchingResult();
        matchingResult.match(stringCrews, level, crews);
        return matchingResult;
    }

    private void shuffle() {
        Collections.shuffle(stringCrews);
    }

}
  • 하지만 여러 매칭이 동시에 실행된다고 했을때 시간효율이 안좋음 + 멀티스레드 환경에서 동시 접근안됨

equals와 hashcode 재정의할때 왜 둘다해야할까?

public class MatchingInfo {
    private Course course;
    private Level level;
    private Mission mission;

    public MatchingInfo(Course course, Level level, Mission mission) {
        this.course = course;
        this.level = level;
        this.mission = mission;
    }

    @Override
    public boolean equals(Object o) {
        if(this == o) return true;
        if(o == null || getClass() != o.getClass()) return false;
        MatchingInfo matchingInfo = (MatchingInfo) o;
        return course == matchingInfo.course &&
                level == matchingInfo.level && mission == matchingInfo.mission;
    }

    @Override
    public int hashCode() {
        return Objects.hash(course, level, mission);
    }
}
  • 해쉬코드는 객체의 주소값을 변환하여 생성한 객체의 고유한 정수값
  • 실제 객체들의 주소가 다르더라도 변환된 hashCode를 보고 같은 객체인지 판단함!
  • 그래서 String 같은 경우도 문자열이 같으면 hashCode가 같다

key 결과에 따라서 Dto 보다는 matchingResult 가 저장되었으면 하는데 service에서 가져오도록 하려면 로직을 전부 수정해야함 → 잘못된 코드 아닌가

private final HashMap<MatchingInfo, MatchingResult> repository = new HashMap<>();
  • 원래 MatchingResultDto 였는데 MatchingResult로 수정
  • service 에서 Dto를 반환받도록 되어있어서 결과값을 수정하려면 모두 다 변경해야함
  • 테스트코드를 작성하면서 진행했으면 좀 더 수월했을 것 같음!

Matching을 MatchingResult와 분리 시켰는데 MatchingResult public method 들이 불안함

  • public 메소드를 어쩔수 없이 사용하는 경우 접근 제한을 어떻게 할까?
  • 패키지로 접근 제한하나?

엔티티에서 Dto를 만들어서 반환할까? 아니면 service에서 get으로 엔티티 내부 변수 가져와서 Dto로 변환할까?

public MatchingResultDto convertDto() {
        return new MatchingResultDto(matchingResult);
    }
  • getter 안쓰려면 안에서 이렇게 해줘야할 것 같다

단위 테스트 진행할때 접근제어자 public에서 private으로 변경되는 경우는 단위 테스트 진행 못하나요? (matching에서 기능들 private으로 변경해서 이전에 테스트한 것들에 에러생김)

  • 굳이 private 까지 테스트를 진행할 필요가 없다고 한다...!
  • private를 할 수 있는 방법은 있다
  • 쓸데없는 테스트때문에 효율도 떨어지고 코드량도 많아지는 문제가 있을 것 같다

MatchingTest에서 단위 테스트 진행할때 공통되는 부분이 많다(given, when, then)

class MatchingServiceTest {

    @Test
    public void matchTest() {
        // given
        Course course = Course.BACK_END;
        Level level = Level.LEVEL1;
        Matching matching = new Matching(course.getStringCrews());
    }
}
  • 공통된 부분은 @Before 를 사용하여 setup해서 진행하자

회고

우테코 최종 코테였던 미션이다. 처음 풀때 엄청 어려웠는데 지금은 꽤나 잘 풀 수 있는 것 같다.

고민없이 공부하는 게 문제인 것 같고 어떤 방법이 더 좋은지 끊임없이 생각하고 적용해보면서 장단점을 체화하는게 좋은 것 같다. 더 나아가서 이유에 대해서 논리적으로 설명할 수 있으면 좋겠다.

끝.

우아한테크코스에서 진행하는 미션을 혼자 진행해보면서 나의 생각과 배운 것을 정리하는 글

[로또미션]

[내코드PR]

 

[로또 - 1단계] 미션 제출합니다. by changjun6518 · Pull Request #2 · changjun6518/java-lotto-1

안녕하세요! 😊 로또 미션을 구현해보았습니다! 🎓 구현하면서 조금이라도 궁금하고 의문이 들었던 생각들을 정리하고 코멘트에 남겼습니다! 피드백 주시면 저에게 큰 도움이 될 것 같습니다!

github.com

 


금액 단위를 검증하려는 메소드명 뭐로할까?

  • 별 생각없이 isValid~~로 작성했지만 boolean 변수 이름을 제대로 짓기 위한 컨벤션..? 이 있었다
  • is는 boolean 타입을 반환할 것이라는 생각을 들게하기 때문에 적절하지 않은 것 같다

Amount의 isValidDigit 들여쓰기 2칸 어떻게 해야할까?

  • 들여쓰기 제한을 1로 하고 싶어서 고민했는데 도저히 생각이 안났다..! 따로 함수를 분리해도 되는데 함수명을 어떻게 나눠줘야할지 또 나누는 게 좀 불필요하다는 느낌이 많이 들었다
  • 이 함수안에서만 국한되지 말고 amount를 String으로 받아올때 생기는 문제인데 애초에 Integer로 받아오게 되면 Digit인지 확인할 필요가 없다. 아니라면 컴파일 에러가 날 것이다

 


검증 함수를 만들때 반환타입을 boolean으로 하는지 아니면 void 로 하고 바로 throw new 날리는지?

    • 아무 생각없이 코딩하다가 예외처리를 어떻게 해야할지 의문이 들었다
    • 위 방식대로 조건문에 해당하면 throw new Exception()을 처리해줬다
    • 나중에는 같은 Exception끼리 묶고 Exception을 상속?받는 등 처리해서 예외처리에 네이밍을 해줄 수 있을 것 같다
    • 하나하나 예외처리 클래스를 생성해서 만드는지 ENUM을 활용하는지는 더 공부해봐야 겠다

InputView 에서 입력받는 라이브러리? 어떻게 사용했는가?

  • 입력 기능을 하는 Scanner 클래스를 한번만 생성해도 되니까 상수?처리를 해줬다
  • 변수명을 sc라고 했는데 SCANNER로 상수 컨벤션을 지켜야하는 사실을 알게되었다...이것도 해당하는 게 맞겠지?

예외처리 발생 테스트 코드 작성 어떻게 하지?

  • 요렇게!
  • Assertions.assertThrows(Exception.class, () -> )
  • () -> 는 예외가 발생하는 상황을 만들어주면 된다

전체적인 연결 기능은 기능목록을 작성하지 않는 걸까?

  • 이건 클래스마다 하나씩 단위 테스트 진행하면서 했는데 전체적인 클래스들의 연결 테스트는 이뤄지지 않고 또 어떻게 테스트를 해야할지 모르겠다
  • 지금 미션은 콘솔 미션이지만 웹에서는 이런 상황을 테스트하기 위해 RestAssured 라는 라이브러리?가 있다고 한다

멤버변수 생성 시점을 어디서 하는게 좋을까?

  • 정답은 없고 상황에 따라서 어떤 방식을 선택할지 다를 것 같은데
  • 최근에 공부한 토비의 스프링에서 인터페이스를 공부했는데 인터페이스를 통해 기능 구현에 대한 분리를 이뤄냈지만 어떤 클래스가 필요한지에 대한 관심을 분리하지 못하여 생기는 문제를 상위객체(클라이언트 객체)에서 생성해서 주입하면서 이 관심을 상위객체에게 넘겨버릴 수 있었다
  • 아무튼 지금 상황에서는 크게 중요하지 않지만 스프링 생태계로 넘어가면 보통 생성자를 통해 주입받는 형태로 사용하게 되지 않을까 싶다. 물론 그때도 이유와 근거는 명확한 게 좋겠지

 


Lotto 에서 랜덤넘버를 생성하고 중복되지 않으면 숫자를 넣는 함수명은?
Lotto 번호가 잘 생성됐는지 확인하고 싶은데...! 이 부분은 어떻게 테스트할까?

 

  • 로또는 모두 중복되지 않은 숫자인 걸 알고 있으니 addRandomNumber 또는 addNumber 정도만 해도 충분할 것 같다
  • 고민해보니 이 모든 고민과 컨벤션과 클린코드와 왜 좋은 코드를 만드려고 하는가에 대한 본질은 미래의 나와 다른 개발자가 내 코드를 다시 봤을때 보다 빠르게 이해하고 알아보기 쉽게 하기 위해서라는 걸 생각하게 됐다
  • 랜덤 번호 확인은 자바 라이브러리에서 제공하는 것이니 믿고 넘어가도 될 것 같다

밖에서 new 생성자를 만들어서 사용하는 게 좋을까?

  • 생성자와 정적 팩토리 메소드
  • 사실 이부분에서 정적 팩토리 메소드를 사용하는 장점이 없는 것 같다
  • 팩토리 메소드 패턴은 상위 하위 클래스가 있고 하위 클래스에서 생성 로직을 따로 구현하는 방식으로 알고 있다

Lotto 생성하는 로직을 static으로 generator를 빼주는게 왜 좋을까?

  • 이것도 Lotto에 생성 로직이 있는게 맞는가 라는 생각
  • 한번만 생성한 뒤로 Lotto안에 생성하는 메소드는 다시 안쓰이는데 불필요한 코드만 차지하는 느낌
  • 빼주는 것도,,, 나쁘지 않을 것 같다 더 많은 기능들이 추가된다면

Lotto 번호와 winning 번호 비교하는 로직을 만드는데 어떻게 하는게 좋을까?
Lotto에 compare 메서드 들여쓰기 2개 어떻게 풀어낼까?

  • 객체에 메세지를 보내면서 비교하도록 했다
  • 근데 메세지를 타고타고 많이 보내게 되면서 함수명도 똑같고 어떤 객체부터 최종 객체까지 메세지를 보내는 과정이 헷갈리는 문제가 생겼다
  • 이런 경우에는 getter로 타협해도 괜찮을 것 같다는 생각이 들었다
  • 다음에도 문제 생길때 고민해보자!
  • 두번째는 들여쓰기를 위해 함수분리해도 괜찮을 것 같다

주요 로직을 테스트 코드로 먼저 작성하고 프로덕션 영역에 옮겨 작성하는지?

  • 테스트 주도 개발은 테스트부터 작성하고 프로덕션 코드에 옮기고 불필요한 테스트 코드를 삭제하는 방식으로 진행할 것 같다...!
  • 아직 테스트 코드부터 작성하는 게 익숙하진 않다. 맞는건지도 잘 모르겠고!

Reward 에서 카운트와 일치하는 것 찾아주는 로직 어떻게 하지?
테스트가 잘 작동하는지 확인하려고 print문 잠시 넣어서 확인했다 더 좋은 방법이 없을까?

  • Reward enum에서 카운트 갯수를 메세지로 받으면 일치하는 enum을 반환해주는 메소드를 만들었다
  • 테스트 잘 동작하려는지 보려고 print 메소드를 임시로 만들었는데 콘솔이라 그런가 분명 방법이 있을텐데 더 찾아보자
  • 비교 결과 reward를 확인하고 싶어서 print를 한건데 상황 만들어서 상황에 따른 결과 reward가 맞는지 확인하면 될 것 같다

테스트 함수명 카멜케이스, 스네이크 케이스?

  • 카멜케이스 사용하는 것 같다!

getter를 사용하지 않기 위해서 객체에 메세지를 던지는데 서로 던지다보니 메서드 명이 일치해서 좀 헷갈리는 현상 발생

  • 아까 언급한 부분인데 winningnumber와 lotto와 비교하는 메소드인데 객체에 메세지를 넘겨주다보니 계속 해서 넘겨주게 되고 메소드명도 compare로 일치하니까 어디서 어떻게 넘어가는지 헷갈림
  • 사실 getter 써도 넘어가는 건 똑같은데 꺼내왔다는 의미로 생각하면 괜찮은데 compare는 어떨지..?
  • compare도 똑같은 거 같은데?? 단지 의존 관계라고 해야하나 그 부분이 헷갈린다

Result returnProfit 메서드 언제 기본형 타입쓰고 언제 래퍼 타입을 사용하는지?

  • Wrapper 클래스를 사용하는게 좋다는 글을 본 거 같은데.. 이건 코딩을 많이하고 다른 사람들 코드를 많이 보게 되면 저절로 알게 될 것 같은데
  • Wrapper 쓰면 컴파일 에러를 더 잘 잡아주지 않을까..? 똑같나..?
  • 추가적인 기능을 더 사용할 수 있다!!

OutputView 에서 Reward enum을 foreach 했을때 Reward.FAIL까지 출력되는 문제 발생

  • Enum의 foreach 인데 Stream을 활용해서 아주 깔끔하게 리팩토링했다..!

 


회고

우테코 4기 최종 탈락한 뒤로 혼자서라도 해야겠다는 생각에 무작정 미션을 진행해봤지만 성장하는 느낌이 들지 않았다

방법을 바꿔서 미션을 진행하면서 생기는 문제들을 모두 기록하고 스스로 고민해보자는 생각을 했고 git을 통해 나쁘지 않게 진행한 것 같다

그리고 꽤나 많은 걸 얻어가는 기분이다. 또 우테코 출신 많은 분들이 블로그로 회고를 잘 정리해둔 것을 참고하여 더 배워갈 점이 많았다.

근데 진행하면서 3주에서 4주정도 로또만 했는데 많은 시간한 것도 아니고 시간만 질질 끈 기분이다.

데드라인을 정하고 알차게 시간을 써야할 것 같다.

다음은 페어 매칭 프로그램 미션을 진행해볼까 한다.

참고

[참고한 우테코회고블로그]

피로도

https://www.acmicpc.net/problem/22864

 

22864번: 피로도

첫 번째 줄에 네 정수 $A$, $B$, $C$, $M$이 공백으로 구분되어 주어진다. 맨 처음 피로도는 0이다.

www.acmicpc.net


접근

  • 24시간 동안 피로도 M을 넘기지 않고 최대한 많이 일을 할 수 있는지 구하는 문제
  • 24번 반복문을 돌리면서 현재 피로도 + A ≤ M 인지 파악하면서 가능하면 일하고(+ A) 아니면 쉬게한다(- C)
  • 일할때마다 총 일하는 크기를 더해간다(+ B)
  • 피로도가 음수로 내려가면 0으로 바꿔준다

코드

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Scanner;
import java.util.StringTokenizer;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        int A, B, C, M;
        int tiredness = 0;
        int answer = 0;
        A = Integer.parseInt(st.nextToken());
        B = Integer.parseInt(st.nextToken());
        C = Integer.parseInt(st.nextToken());
        M = Integer.parseInt(st.nextToken());

        for (int i = 0; i < 24; i++) {
            if (tiredness + A <= M) {
                tiredness += A;
                answer += B;
            } else {
                tiredness -= C;
                if (tiredness < 0) {
                    tiredness = 0;
                }
            }
        }

        System.out.println(answer);
    }
}

회고

  • 문제를 잘 읽도록 하자. 다소 쉬운 문제지만 문제를 꼼꼼히 읽지 않아서 생기는 오류가 많았다. 예를 들면 피로도가 음수일때 0으로 한다, 피로도가 M을 넘기면(≤) 번아웃이다.
  • 시간 복잡도 : O(1)
  • 공간 복잡도 : O(1)

우아한테크코스에서 진행하는 미션을 진행해보면서 나의 생각과 배운 것을 정리하는 글

[로또미션]

[내코드PR]

 

[로또 - 1단계] 미션 제출합니다. by changjun6518 · Pull Request #2 · changjun6518/java-lotto-1

안녕하세요! 😊 로또 미션을 구현해보았습니다! 🎓 구현하면서 조금이라도 궁금하고 의문이 들었던 생각들을 정리하고 코멘트에 남겼습니다! 피드백 주시면 저에게 큰 도움이 될 것 같습니다!

github.com

 


기능목록 작성 후에 처음 작성하는 코드는 뭘까?

    • 다들 어느정도 필요한 클래스를 구상하고 테스트 코드부터 작성하는 식으로 진행하는 듯 했다.

입력값은 보통 String 값인데 클래스 멤버변수로 다른 타입을 가질때 어떻게 할까?

  • InputView 영역 즉, 프론트엔드 쪽에서 정해진 형식을 통해 보내질 것이기 때문에 String이 아닌 Integer를 매개변수로 받아와도 될 것 같다.
  • 하지만 프론트엔드 뿐만 아니라 백엔드 쪽에서도 검증하는 함수가 필요하다.
  • 생각해보니 amount에 문자가 들어갈 경우 Integer.parseInt() 오류를 처리하기 난처했는데 매개변수로 Integer로 받기 때문에 컴파일?시에 오류가 나서 사전에 오류를 방지할 수 있다!
  • 이렇게 될 경우 위 isValidDigit 검증함수는 불필요하게 될 것 같다.

값 복사는 왜 해야할까? ( 위 Amount 클래스 생성자에서 검증 로직을 거치기 전에 값부터 복사하기 )

  • 위 예시로 보면 검증로직을 거치고 마지막에 this.amount = amount 를 하여 대입을 하게 된다.
  • 이렇게 진행하면 멀티스레드 환경에서 this.amount 에 대입 직전에 다른 스레드에 의해 매개변수가 가리키고 있는 값이 바뀔 가능성이 존재할 수 있다는 문제가 있다.

  • 생성자가 실행되면 생성자를 실행시킨 함수에 있는 amount(Stack에 있는)의 주소가 넘어온다.
    • 이때 주소가 넘어오는지에 대한 정확한 지식이 없다... 토론해보았지만 해답을 얻지 못했다.
    • JVM + 스택, 힙 변수 생성 과정 + 기본형 변수, Wrapper 클래스 생성 등
  • 하나의 스레드가 그 주소에 있는 값(10000)을 확인하면서 검증한다.
  • 이때 다른 스레드가 amount 값을 변경시킨다. (예를 들어 5000으로 변경)
  • 그러면 처음 스레드가 검증을 끝난 뒤 검증할때 값 10000 이 아닌 5000을 instance의 amount에 복사하게 된다.
  • 이러한 문제를 방지하기 위해 값부터 복사하고 복사한 값을 검증하는 식으로 하는 것 같다.

 


회고

오전에 하는 스터디사람들과 나의 궁금점들을 공유했다.

시험삼아 해보았지만 생각보다 많은 부분을 토론하게 되었고 혼자 고민했던 것보다 3배는 더 많은 것을 얻은 느낌이였다. 

앞으로 궁금증들을 모두 적고 정리하고 공유하면서 토론을 진행해보려고한다.

+ 우테코 미션 회고들(이전 기수들)을 참고하면서 배울 점들을 쏙쏙 빨아보려한다.

 

참고

[값복사]

[스택&힙]

 

https://www.acmicpc.net/problem/1463

 

1463번: 1로 만들기

첫째 줄에 1보다 크거나 같고, 106보다 작거나 같은 정수 N이 주어진다.

www.acmicpc.net

 

 


 

 


 

  • 접근

처음에는 while 반복문 돌리면서 3으로 나누기, 2로 나누기, 1빼기 순으로 각 조건을 확인해서 count를 세는 간단한 문제인줄 알았다.

 

하지만 두번째 테스트 케이스를 보면 10의 경우 10 -> 9 -> 3 -> 1 순으로 3번 만에 만들 수 있지만 위 방식으로 풀게되면 10 -> 5 -> 4 -> 2 -> 1 (2로 나누기 부터 적용되기 때문에) 4번으로 최소 횟수가 안되는 함정이 있다.

 

다시 접근해보면 메모이제이션을 이용한 재귀를 떠올려볼 수 있다.

 


  • 코드
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int N = sc.nextInt();
        Integer[] arr = new Integer[1000001];
        arr[0] = arr[1] = 0;
        System.out.println(recur(arr, N));
    }

    public static int recur(Integer[] arr, int N) {
        if (arr[N] == null) {
            if (N % 6 == 0) {
                arr[N] = Math.min(recur(arr, N - 1), Math.min(recur(arr, N / 3), recur(arr, N / 2))) + 1;
            } else if (N % 3 == 0) {
                arr[N] = Math.min(recur(arr, N / 3), recur(arr, N - 1)) + 1;
            } else if (N % 2 == 0) {
                arr[N] = Math.min(recur(arr, N / 2), recur(arr, N - 1)) + 1;
            } else {
                arr[N] = recur(arr, N - 1) + 1;
            }
        }
        return arr[N];
    }

}

  • 해설

6으로 나눠떨어지는 경우 3가지 연산 방법 중 최소값을 찾아가야한다.

3으로 나눠떨어지는 경우는 2로 나눌수 없으니 2 나누기를 제외한 2가지 연산 방법 중 최소값을 재귀호출!

2으로 나눠떨어지는 경우도 3으로 나누는 것과 마찬가지!

이외에 나눠떨어지지 않으면 1을 빼는 연산 방법을 마지막으로 재귀호출한다.

 

  • 다른 풀이

재귀 함수 매개변수를 통해 count 값을 넘겨주면서 진행하는 방법이 있다.

 


  • 회고

메모이제이션까지 방법을 생각해냈지만 구현까지 성공하지 못했다.

계속 문제를 풀면서 구현 능력을 키우면 쉽게 구현할 수 있을 것 같다.

시간 복잡도 O(N)...?

공간 복잡도 O(N)

바텀 - 업 방법으로도 구현해보자

 


  • 참고

https://st-lab.tistory.com/133

https://www.acmicpc.net/problem/10828

 

10828번: 스택

첫째 줄에 주어지는 명령의 수 N (1 ≤ N ≤ 10,000)이 주어진다. 둘째 줄부터 N개의 줄에는 명령이 하나씩 주어진다. 주어지는 정수는 1보다 크거나 같고, 100,000보다 작거나 같다. 문제에 나와있지

www.acmicpc.net

문제 분석

  • 명령어로 동작하는 정수를 저장하는 스택 구현기

접근

  • 각 명령어에 대한 함수 만들고
  • 클래스 만들고 멤버 변수로 스택명령어 입력 받는 함수 만들기

오류

  • 시간초과 오류났는데 입력받는 시간이 오래걸린다고 함 Scanner -> BufferedReader 사용
    • Scanner는 원시타입과 String 파싱 등 정규 표현식 적용, 입력값 분할, 파싱 과정을 거치기 때문에 낮은 퍼포먼스
    • BufferedReader는 입력을 버퍼에 모아두었다가 한번에 그 내용을 전달하기 때문에 빠른 속도
    • 왜 더 빠를까?
    • Scanner도 버퍼로 입력받는걸로 이해했는데...?
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Scanner;
import java.util.Stack;

public class Main {
    static BufferedReader bf = new BufferedReader(new InputStreamReader(System.in));

    public static void main(String[] args) throws IOException {
        StackClass stackClass = new StackClass();
        int totalCommandCount = Integer.parseInt(bf.readLine());

        for (int i = 0; i <= totalCommandCount; i++) {
            stackClass.execute(bf.readLine());
        }
    }

    private static class StackClass {
        private Stack<Integer> stack = new Stack<>();

        public StackClass() {
        }

        public void execute(String command) {
            if (command.startsWith("push")) {
                String[] split = command.split(" ");
                push(Integer.parseInt(split[1]));
            }

            if (command.equals("pop")) {
                pop();
            }
            if (command.equals("size")) {
                size();
            }
            if (command.equals("empty")) {
                empty();
            }
            if (command.equals("top")) {
                top();
            }
        }

        private void push(int x){
            stack.push(x);
        }

        private void pop() {
            if (stack.isEmpty()) {
                System.out.println(-1);
                return;
            }
            System.out.println(stack.pop());
        }

        private void size() {
            System.out.println(stack.size());
        }

        private void empty() {
            if (stack.isEmpty()) {
                System.out.println(1);
                return;
            }
            System.out.println(0);
        }

        private void top() {
            if (stack.isEmpty()) {
                System.out.println(-1);
                return;
            }
            System.out.println(stack.peek());
        }
    }
}

회고

  • 이번 문제도 if문을 나열해서 풀었는데 가독성 면에서는 스위치문이 좋고 성능면에서는 else if가 좋을것 같다는 피드백을 받았다
  • 확실히 스위치문으로 보니 가독성이 올라간다고 느껴졌다
import java.io.*;
import java.util.*;

public class Main {
    public static void printValue(Integer value) {
        System.out.println(value == null ? -1 : value);
    }

    public static void main(String[] args) throws IOException {
        Deque<Integer> deque = new LinkedList<>();
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int N = Integer.parseInt(br.readLine());
        StringTokenizer st;
        for (int i = 0; i < N; i++) {
            st = new StringTokenizer(br.readLine());
            String command = st.nextToken();
            switch (command) {
                case "push_front":
                    deque.addFirst(Integer.parseInt(st.nextToken()));
                    break;
                case "push_back":
                    deque.addLast(Integer.parseInt(st.nextToken()));
                    break;
                case "pop_front":
                    printValue(deque.pollFirst());
                    break;
                case "pop_back":
                    printValue(deque.pollLast());
                    break;
                case "size":
                    printValue(deque.size());
                    break;
                case "empty":
                    printValue(deque.isEmpty() ? 1 : 0);
                    break;
                case "front":
                    printValue(deque.peekFirst());
                    break;
                case "back":
                    printValue(deque.peekLast());
                    break;
            }
        }
    }
}

'알고리즘 > 자료구조' 카테고리의 다른 글

[백준] 10799 쇠막대기 - 자바  (0) 2022.02.01

https://www.acmicpc.net/problem/10799

 

10799번: 쇠막대기

여러 개의 쇠막대기를 레이저로 절단하려고 한다. 효율적인 작업을 위해서 쇠막대기를 아래에서 위로 겹쳐 놓고, 레이저를 위에서 수직으로 발사하여 쇠막대기들을 자른다. 쇠막대기와 레이저

www.acmicpc.net

분석

  • 레이저는 반드시 인접한 () 쌍으로 표현
  • 나머지 떨어진 괄호`()`가 쇠막대기로 표현됨
  • 잘려진 쇠막대기는 총 몇조각인가? 를 구하는 문제

접근

  • 스택으로 하는게 좋을 것 같음
  • ( 나오면 스택에 넣고
  • ) 나오면 pop() 하고 스택 사이즈 만큼 더해주면 될 것 같음
  • 예시 시뮬레이션을 돌려보니 3 + 3 + 2 + 3 + 2 + 2 + 1 + 1 = 17 맞음

오류

  • 두번째 예시 틀렸음
  • )로 레이저 말고 쇠막대기를 닫을때는 stack size 만큼이 아니라 +1 해주면 된다
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Stack;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String parentheses = br.readLine();
        Stack<Character> stack = new Stack<>();
        int count = 0;
        char prev = '(';
        for (char c : parentheses.toCharArray()) {
            if (c == '(') {
                stack.push(c);
            }
            if (c == ')') {
                if (prev == '(') {
                    stack.pop();
                    count += stack.size();
                } else {
                    count++;
                    stack.pop();
                }
            }
            prev = c;
        }

        System.out.println(count);
    }
}

 

회고

  • if 문을 왜 나열했을까? -> else if()를 사용해서 하나만 체크하고 넘어가는 식으로 만드는 것이 오류를 줄일 수 있을 것 같다
  • stack.pop()이 공통으로 중복되므로 하나로 빼서 중복을 줄여줄 수 있었다

'알고리즘 > 자료구조' 카테고리의 다른 글

[백준] 10828 스택 - 자바  (0) 2022.02.01

+ Recent posts