-
Notifications
You must be signed in to change notification settings - Fork 249
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[1단계 - 로또 구현] 현구막(최현구) 미션 제출합니다. #275
Conversation
[TDD] 테스트 코드 우선 작성 - 5내용
링크[OOP] 원시값 포장 - 3내용
링크[OOP] 일급 컬렉션 - 4내용
링크[JDK] Enum - 5내용
링크[JDK] Stream API - 3
링크 |
안녕하세요 현구막! 리뷰어는 아니지만 방어적 복사에 대해 참고할만한 링크 하나 드릴게여 제 의견을 드리자면 현구막이 고민하고 있는 상황에서 리스트를 복사할 필요는 없다고 보고 외부에서 오는 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
안녕하세요 현구막, 리뷰어 코니입니다 👋
로또 미션을 요구 사항에 맞게 잘 구현해 주셨어요 👍 꼼꼼한 커밋과 코멘트, 학습로그, 그리고 코드를 보면서 고민을 많이 하신 것이 느껴졌습니다.
질문에 먼저 답을 드릴게요.
- 저도
LottoGame
의 존재는 좋다고 생각해요. 그런데 지금의LottoGame
은 현구막이 느끼시는 것처럼 너무 무겁고 복잡해요. 그러다 보니 테스트 코드를 작성하기도 어려워서 지금 중요한 로직의 테스트가 많이 빠져 있는 상황이네요. 책임을 다른 객체들에게 나눠 주고 테스트 코드도 작성해 보시면 좋을 것 같아요.
그리고 제 관점에서는 그려주신 두 가지 구조가 크게 다르지 않다고 느껴져서요, 언제 뭘 선택한다 이런 말씀을 드리기는 애매하네요 😅 - 일급 컬렉션은 사실 보편적(?)으로 인지되는 개념은 아닌 것 같고요, 따라서 자료도 거의 없다시피 하죠. 가장 좋은 건 아마 현구막도 보셨을 이동욱님의 글인데요, 여기서도 일급 컬렉션의 장점 중 하나로 상태와 행위를 한곳에서 관리를 언급하고 있어요. 일급 컬렉션이 그저 값만 담고 있고 밖에서는 getter로 값을 꺼내 쓰기만 한다면, 코드의 복잡성만 높일 뿐 아무 장점을 누리지 못하는 거라고 생각해요.
- 리뷰어마다 의견이 다를 수 있는데 저는 필요한 곳에서만 하면 된다고 생각하는 편이에요. 지금은 학습 단계이니 그 필요성과 사용법을 배우는 것을 목표로 사용해볼 수는 있겠지만, 후자의 경우는 좀 과하다고 생각합니다. 이건 코멘트로도 남겨 드렸어요.
추가적으로 궁금한 점이나 의견이 있다면 언제든 코멘트나 DM 주세요!
} | ||
|
||
public void buyTickets(final Money money) { | ||
this.initMoney = new Money(Integer.toString(money.getValue())); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이건 방어적 복사인 건가요? 여기에서 이렇게까지 할 필요는 없어 보여요 😅
public Money(final String money) { | ||
this.value = Integer.parseInt(money); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
문자열 파싱 정도는 밖에서 해 준다면 도메인은 핵심 로직에 더 집중할 수 있을 것으로 보여요!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
아!!!!!!!!!!!
매 미션마다 '사용자의 입력에 따른 정수 변환은 Integer.parseInt
가 걸리는데...' 라고 고민은 해놓고 저런 식으로 구현을 진행했었는데, 말씀해주신대로 컨트롤러나 LottoGame
쪽에서 파싱 후 넘겨주면 간단하게 해결이 가능했었네요..
"원시 값을 감싸는 이유는 믿고 편하게 사용하기 위함이다." 라는 생각에 갇혀서 문자열로 넘어오는걸 처리해주는게 당연한 역할이라고 생각해왔습니다. 머리가 탁 트인 기분이에요!!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
추가적으로 궁금한게 있어요!
외부에서 정수로 파싱된 데이터를 넘겨주게 될텐데, 이 때 파싱이 불가능한 문자열 혹은 공백을 검증, 파싱하는 로직은 LottoGame
이나 Controller
에서 진행하게 되는건가요? 아니면 View
단에서 진행하게 되는 것인가요?
LottoGame
이나 Controller
가 검증 로직을 갖기에는 안그래도 무거운 책임을 더해주는 것 같고,
View
단에는 검증, 파싱 ‘로직’을 가지면 안되는거 아닌가 고민이 돼요.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DM으로도 같은 질문을 주셨기에 답변 드렸습니다~
그런데 이건 제 의견일 뿐이라서 당장 다음 미션에서 다른 리뷰어님이 저와는 상충하는 의견을 주실 수도 있어요. 이런 정답이 없는 문제에서는 리뷰어의 의견을 참고로 해서 본인만의 의견을 만들어 가시는 게 좋을 것 같습니다.
다시 한 번 강조할게요. 꼭 제 의견을 받아들여 수정하셔야 하는 건 아니에요!
} | ||
} | ||
|
||
public boolean isCanBuyAmount(final int amount) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이름에서 is
는 빠져도 될 것 같아요.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is
가 boolean 타입을 반환하는 메서드 네이밍 컨벤션이 아닌가 했는데,
자세히 알아보니 can, has, should
등이 같은 의미를 가지는군요!! 감사합니다!!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이미 보셨을지 모르겠지만, Bool 변수 이름 제대로 짓기 위한 최소한의 영어 문법 추천드립니다 ㅎㅎ
|
||
public LottoTicket() { | ||
this.lottoNumbers = new LottoNumbers(); | ||
for (int number : issueLottoNumbers(new ArrayList<>())) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
빈 ArrayList
가 여기서 생성되어 issueLottoNumbers()
, initNumbers()
까지 거쳐 들어가며 채워지고 있군요. 조금 더 단순해질 수도 있을 것 같아요. 그리고 제 생각엔 이렇게 로또 번호를 발권하는 역할은, 이름으로만 봐서는 LottoTicket
보다 LottoTicketIssueMachine
이 가지는 것이 더 자연스러울 것 같은데 현구막은 어떻게 생각하시나요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
로또 번호를 찍는 것이 로또 티켓이 가져야할 행동이라고 생각했어요. 그런데 코니의 피드백을 토대로 생각해보면
-
발급 행위는 "1~45 번호를 만들어서", "갖는 것"
만드는 행위를LottoTicket
이 해버리면 사실LottoTicketIssueMachine
이 존재할 이유가 크게 없다. -
현실 세계에서
LottoTicket
은LottoNumber
들의 묶음이다.
LottoNumbers == LottoTicket
이므로,LottoTickets
를 없앨 수 있다.
LottoTicket
이LottoNumber
의 일급 컬렉션이 될 수 있다.
까지 생각이 정리되는 것 같아요.
import java.util.Collections; | ||
import java.util.List; | ||
|
||
public class LottoTickets { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 일급 컬렉션은 하는 일이 별로 없네요. toList()
메서드에 의해 List<LottoTicket>
이 리턴되고 있는데 여기부터 시작해서 일급 컬렉션에서 값을 직접 꺼내 쓰는 일이 연쇄적으로(LottoTickets
→ LottoTicket
→ LottoNumbers
→ LottoNumber
) 일어나고 있어요. 값을 꺼내 쓰기보다 메시지를 보내는 방식으로 바꿔볼 수 있을까요? 그렇게 하면 어떤 장점이 있을까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
객체간의 참조를 줄이고, 내부 구현을 드러내지 않을 수 있네요.
객체에 메세지를 보내라를 알고 있음에도 항상 응용하지 못하는건 생각의 시작점이 문제인 것 같습니다.
"출력뿐만 아니라 포괄적으로 사용가능한 toList()
가 좋겠어!"
식으로 생각을 시작하면 안되고,
"출력을 위해 Controller가 로또 번호가 나열되는 리스트(혹은 문자열)를 달라고 요청시켜야겠군"
식으로 필요한 것만 최소한으로 구현하는 것으로 생각을 시작해야 하는걸까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
출력을 위한 getter는 불가피한 면이 있다고 생각해요. 다만 도중에 굳이 값을 직접 꺼내지 않아도 되는데 꺼내는 경우(아마도 주로 연산이나 비교를 위해서?)가 있다면 메시지를 보내는 방향으로 개선해 볼 수 있을 것 같아요.
public static LottoNumbers issueWinNumbers(final String numbers) { | ||
LottoNumbers lottoNumbers = new LottoNumbers(); | ||
for (String number : numbers.split(DELIMITER)) { | ||
lottoNumbers.add(new LottoNumber(number)); | ||
} | ||
return new LottoNumbers(lottoNumbers.toList()); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
여기도 방어적 복사인가요? 마찬가지로, 한 메서드 안에서 이렇게까지 할 필요는 없어 보여요 😳
|
||
private void validateDuplicate(final List<LottoNumber> lottoNumbers) { | ||
if (new HashSet<>(lottoNumbers).size() != lottoNumbers.size()) { | ||
throw new IllegalArgumentException("잘못된 당첨번호 입력입니다."); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 예외가 발생할 일은 테스트 코드에서 말고는 없네요. 실제 어플리케이션에서 중복 당첨 번호가 들어오면 아래에 있는 add()
메서드에 의해 먼저 예외가 발생합니다.
public void add(final LottoNumber lottoNumber) { | ||
if (contains(lottoNumber)) { | ||
throw new IllegalArgumentException("중복된 당첨번호가 존재합니다."); | ||
} | ||
lottoNumbers.add(lottoNumber); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- 생성자의
validateDuplicate()
에 의해 중복이 확인되어야 할 것 같은데, 위에서 말씀드린 것처럼 실제로는 여기서 예외가 발생하고 있어요. 이 설계에서의 문제점은 무엇일까요? - 생성자만 사용하지 않고 이 메서드를 따로 만드신 이유가 무엇인가요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-
첫 번째 문제로 생성자를 저렇게 만들었다면 이미 외부에서 완성된 List를 넘겨줬어야 했을 것 같고,
두 번째 문제로 List 앞에final
키워드를 붙인 것은 "원소 불변을 원한다는 일종의 표식"으로 사용한 것인데, (실제론 List 재할당만 막을뿐 원소 불변을 지키진 못하니까) 정작add
메서드를 제공해서 이를 지키지 못했습니다. -
로또 티켓과 당첨번호 모두 번호 6자리를 사용하므로,
LottoNumbers
객체를 재활용하려했습니다. 하지만 지나치게 복잡하기만 해졌네요... 😭
assertThat(lottoTicket.getLottoNumbers().size()).isNotEqualTo(5); | ||
assertThat(lottoTicket.getLottoNumbers().size()).isEqualTo(6); | ||
assertThat(lottoTicket.getLottoNumbers().size()).isNotEqualTo(7); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
assertThat(lottoTicket.getLottoNumbers().size()).isNotEqualTo(5); | |
assertThat(lottoTicket.getLottoNumbers().size()).isEqualTo(6); | |
assertThat(lottoTicket.getLottoNumbers().size()).isNotEqualTo(7); | |
assertThat(lottoTicket.getLottoNumbers().size()).isEqualTo(6); |
이거면 충분하지 않나요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
'경계 값을 검사해야한다.' 는 생각에 저런 코드를 작성했는데, 사실 isEqualTo(6)
자체가 6 이외엔 모두 실패할테니 의미가 없는 라인들이었네요... 깊게 생각하지 않은게 뻔히 보이네요 😭
public class LottoWinNumberIssueMachineTest { | ||
|
||
@Test | ||
@DisplayName("당첨 번호를 문자로 만들려고 할때") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
테스트 이름은 "~ 할 때 ~ 한다"는 식으로 구체적으로 작성해 주세요!
add issueNumbers logic in LottoTicketIssueMachine add random lotto number create logic in LottoNumber add random number create class (RandomUtils)
delete LottoWinConfirmationMachine class delete LottoWinNumberIssueMachine class delete LottoNumbers class add contains method in LottoTicket add LottoWinningNumbers class
add parameter in constructor add validate min amount logic
LottoGame class focus only compare tickets and winning numbers
add get total invenstment money method add get total winning price method
안녕하세요 코니! 리팩토링에 시간이 오래 걸렸습니다! 1. 객체간 결합도?
2. Map<Rank, Integer> 는 포장해도 Map<Rank, Integer>코니가 리뷰해주셨던 연쇄적인 getter부분을 출력에 사용하기 위해 3. 이게 Controller가 할 일인지 LottoGame이 할 일인지...제가 이번에 리팩토링한 그런데 MVC 구조를 다시 생각해보니 " 조금 더 확신을 가질 수 있는 기준이 있을까요?! 4. LottoGameResult 존재 의의
당장 코니가 보시기엔 어떠신가요? 😅 프로의 시선에서 어떻게 보이는지 궁금해요! 감사합니다!! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
안녕하세요 현구막!
피드백을 정말 잘 반영해 주셨네요 👍 코드를 보니까 처음보다 훨씬 쉽고 읽기 편하다는 느낌을 받았는데, 현구막도 그렇게 느끼셨을까요? (저만 그렇게 느꼈다면... 리뷰 실패! 😱) 질문에 대한 답은 하단에 따로 코멘트로 남겼구요, 이번 단계는 여기서 마무리할게요. 2단계에서 만나요!
private LottoWinningNumbers getWinningNumbers() { | ||
Set<Integer> winningNumbers = InputView.inputWinningNumbers(); | ||
int bonusNumber = InputView.inputBonusNumber(); | ||
return new LottoWinningNumbers(winningNumbers, bonusNumber); | ||
} | ||
|
||
private LottoTickets getLottoTickets(Money money) { | ||
LottoTicketIssueMachine lottoTicketIssueMachine = | ||
new LottoTicketIssueMachine(new Money(money)); | ||
return lottoTicketIssueMachine.issueTickets(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
play()
에서 사용되는 순서대로 메서드를 배치한다면 더 읽기 좋아질 것 같아요.
public LottoGame(final LottoTickets lottoTickets, | ||
final LottoWinningNumbers lottoWinningNumbers) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
public LottoGame(final LottoTickets lottoTickets, | |
final LottoWinningNumbers lottoWinningNumbers) { | |
public LottoGame(final LottoTickets lottoTickets, final LottoWinningNumbers lottoWinningNumbers) { |
줄바꿈을 하신 특별한 이유가 있을까요? 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
가로로 너무 길어지는 것 같아서 개행을 했었어요!
return this.lottoTickets.getMatchingResult(this.lottoWinningNumbers, initMachingResults()); | ||
} | ||
|
||
private Map<Rank, Integer> initMachingResults() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
오타!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤭
|
||
private Rank getRank(LottoWinningNumbers lottoWinningNumbers, LottoTicket lottoTicket) { | ||
int matchCount = lottoWinningNumbers.countMatchedWinningNumber(lottoTicket); | ||
boolean isBonusMatch = lottoWinningNumbers.isMatchBonusNumber(lottoTicket); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
boolean isBonusMatch = lottoWinningNumbers.isMatchBonusNumber(lottoTicket); | |
boolean hasBonus = lottoWinningNumbers.hasBonusNumber(lottoTicket); |
이건 어때요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
훨씬 자연스럽고 좋은거 같아요!
private int countBoughtTickets() { | ||
return this.matchingResults.values() | ||
.stream() | ||
.reduce(0, Integer::sum); | ||
} | ||
|
||
public double totalWinningPrice() { | ||
return matchingResults.entrySet() | ||
.stream() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this
키워드를 쓸지 말지 결정해서 하나로 통일해 주세요! (다른 곳에서도요~)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤭...
@DisplayName("맞춘 개수별 등수 확인") | ||
void testGetPrice() { | ||
assertThat(Rank.of(6, false)).isEqualTo(Rank.RANK1); | ||
assertThat(Rank.of(5, true)).isEqualTo(Rank.RANK2); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
테스트가 실패하고 있어요 😱
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
헉... 로직만 고치고 테스트 코드는 안돌려봤네요 ㅠㅠ 반성하겠습니다..
LottoTicketIssueMachine lottoTicketIssueMachine = | ||
new LottoTicketIssueMachine(new Money(3000)); | ||
LottoTickets lottoTickets = lottoTicketIssueMachine.issueTickets(); | ||
assertThat(lottoTickets.getLottoTickets().size()).isEqualTo(3); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
assertThat(lottoTickets.getLottoTickets().size()).isEqualTo(3); | |
assertThat(lottoTickets.getLottoTickets()).hasSize(3); |
😎
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍👍👍 assertThat 테스트 메서드들을 공부해야겠네요!!
Set<LottoNumber> lottoNumberSet = new HashSet<>(); | ||
lottoNumberSet.add(new LottoNumber(1)); | ||
lottoNumberSet.add(new LottoNumber(2)); | ||
lottoNumberSet.add(new LottoNumber(3)); | ||
lottoNumberSet.add(new LottoNumber(4)); | ||
lottoNumberSet.add(new LottoNumber(5)); | ||
lottoNumberSet.add(new LottoNumber(6)); | ||
this.lottoTicket = new LottoTicket(lottoNumberSet); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
여기서도 LottoWinningNumbersTest
에서처럼 스트림을 사용하면 더 좋을 것 같네요 👍
@Test | ||
@DisplayName("총 구입 금액이 일치하는지 확인") | ||
void testSameMatchCount() { | ||
assertThat(lottoGameResult.totalInvestment()).isEqualTo(6*1000); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
assertThat(lottoGameResult.totalInvestment()).isEqualTo(6*1000); | |
assertThat(lottoGameResult.totalInvestment()).isEqualTo(6 * 1000); |
final int startNumber = 1; | ||
final int endNumber = 45; | ||
|
||
for (int i = 0; i < 999999; i++) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😅👍
1: 2, 4: 이미 코멘트로도 남겼던 것처럼 출력을 위한 getter는 불가피하죠. 지금의 3: 지금의 |
안녕하세요 코니! 현구막 입니다. 페어인 포츈과 즐겁게 구현하다보니 시간 가는줄 모르고 제출이 많이 늦었습니다.
아직 많이 부족한 코드지만, 코니가 해주시는 피드백에 따라 열심히 고쳐볼게요!
로또 구현을 진행하면서 궁금했던 것들을 몇 가지 질문드리고 싶어요!
1. MVC에서 Model(domain)의 연결지점
제가 레이싱 게임 미션을 진행하면서까지 생각했던 MVC 구조는 아래와 같았어요.
그런데 포츈과 페어를 진행하면서, 포츈이 작성한 레이싱 게임 Controller 코드를 보고,
'아..! 이 구조가 MVC 관계를 더 명확하게 보여주는 것 같다! Controller가 정말 View-Model 사이의 통신만 도와주네!'
라고 인사이트를 얻어서, 아래와 같은 방식의 MVC 구조로 구현했어요.
그런데 구현이 끝난 상태의 모습을 보니
LottoGameController
는LottoGame
에게 명령만 내리기 때문에 너무 가볍고,LottoGame
은 서비스 로직을 소화하기 때문에 무겁고, 가지고 있는 필드 멤버가 너무 많다는 생각이 들어서 고민이 생기기 시작했어요.두 구조 모두 장단점이 있는 것 같은데, 프로젝트마다 어떤 구조를 채택해야할지 기준점을 알고 싶어요!
2. 일급 컬렉션 클래스는 정확히 컬렉션 역할만?
이번 미션의 주제가 OOP였기 때문에 '클래스 파일을 하나라도 더 쪼개보는게 어떤가' 라는 생각에 우선 컬렉션 역할만 수행하도록 했습니다.
그러다가 '일급 컬렉션 클래스에 서비스 로직이 포함되어도 괜찮지 않을까?' 라는 생각이 들었는데, 막상 서비스 로직이 포함되면
일급 컬렉션 이라는 이름이 안어울리는 것 같고...
확신을 얻을 수 있는 자료가 있을까요?!
3. 방어적 복사는 어느정도까지?
위와 같이 외부 객체와의 연결을 끊기 위한 방어적 복사는 기준이 잡힌 것 같은데,
이렇게 내부 원소만 변하는 경우에도 아래와 같이 방어적 복사를 진행해야 하는지 궁금합니다.
감사합니다!!