본문으로 건너뛰기
code0.HDL

method: value·Action·ActionValue

결과 타입으로 세 종류의 method를 구분하고, rule에서 호출하며, method 자신의 준비 조건이 호출한 rule의 실행 여부를 어떻게 정하는지 확인합니다.

이 장에서 배울 내용

  • 트랜잭션의 계약에서 value·Action·ActionValue method를 고르고 종류별로 정의할 수 있습니다
  • method를 호출하는 rule을 모든 읽기가 진입 상태를 보는 하나의 실행으로 추적할 수 있습니다
  • ActionValue의 결과를 <-로 결합하고, =가 실행되지 않은 동작을 결합하는 이유를 설명할 수 있습니다
  • method 헤더의 if가 호출한 rule의 실행 전체를 보류하는 방식을 설명할 수 있습니다
이 장에서 배우는 개념
  • value·Action·ActionValue method
먼저 알아야 할 개념

1. 결과 타입이 method의 종류를 정합니다

첫 장에서는 value method 하나만 썼습니다. 인터페이스는 모듈의 상태를 바꾸는 method도 제공할 수 있고, 원형의 결과 타입이 어떤 종류인지 알려 줍니다.

결과 타입종류상태 변경반환rule에서 호출하는 법
Bit#(8), Bool 등 값 타입value method없음그 값식이 올 수 있는 모든 곳: c.value
ActionAction method가능없음문장으로: c.bump;
ActionValue#(t)ActionValue method가능t 값<-로: let x <- t.take;

종류는 계약에서 고릅니다. 두 번 호출해도 아무 영향이 없어야 하면 value method입니다. 호출자가 명령을 내리는 것이면 Action method입니다. 반환하는 값과 상태 변경이 떨어질 수 없어야 하면, 즉 누구도 티켓 하나를 읽고 다른 티켓을 예약할 수 없어야 하면 ActionValue method입니다.

인터페이스 하나에 세 종류가 함께 있는 경우가 많습니다. 큐가 대표적입니다.

interface Queue;
  method Action enq(Bit#(8) x);        // 넣기: 상태만 바꿉니다
  method ActionValue#(Bit#(8)) take;   // 꺼내기: 상태를 바꾸고 값을 반환합니다
  method Bool notEmpty;                // 관찰: 값만 반환합니다
endinterface

take를 value method 하나와 Action method 하나로 나누면, 읽은 값과 실제로 꺼낸 값이 같다는 보장을 호출하는 쪽이 떠맡게 됩니다.

확인 문제

어느 모듈이 번호표를 나눠 줍니다. 요청할 때마다 현재 번호를 돌려주고 카운터를 하나 올려서, 두 요청이 같은 번호를 받는 일이 없어야 합니다. request는 어떤 종류의 method입니까?

답

형식

// value method
method 반환_타입 이름(인자) = 식;

// Action method
method Action 이름(인자);
  문장;
endmethod

// ActionValue method
method ActionValue#(t) 이름(인자);
  문장;
  return 식;
endmethod
method Bit#(8) total = sum;

method Action add(Bit#(8) x);
  sum <= sum + x;
endmethod

method ActionValue#(Bit#(8)) drain;
  sum <= 0;
  return sum;
endmethod
정의동작
totalsum의 현재 값을 반환합니다. 상태는 바뀌지 않습니다
addsum의 다음 값을 예약합니다. 반환값은 없습니다
drainsum을 0으로 예약하고, 같은 호출에서 갱신 전 값을 반환합니다

Action method 본문은 action 블록의 축약형이고, ActionValue method 본문은 actionvalue 블록의 축약형입니다. 두 블록은 다음 절들에서 소개합니다. method 정의에 다시 적은 타입은 인터페이스 선언의 타입과 정확히 같아야 합니다.

주의. drain의 return sum은 0이 아니라 갱신 전 값을 반환합니다. <=는 레지스터의 다음 값을 예약할 뿐이고, 같은 호출 안에서 레지스터를 읽으면 언제나 호출이 시작될 때의 값이 보입니다.

2. action … endaction으로 작업 기술하기

타입이 Action인 식은 작업 그 자체가 아니라 수행할 작업의 기술입니다. action 블록은 레지스터 쓰기와 Action method 호출 같은 기본 Action을 원자적인 Action 하나로 묶습니다.

형식

action
  문장;
  문장;
endaction
Action clearBoth = action
  left <= 0;
  right <= 0;
endaction;

rule clear (step == 1);
  clearBoth;
endrule
코드일어나는 일
Action clearBoth = action ... endaction;쓰기 두 개의 기술에 이름을 붙입니다. 아직 아무것도 지워지지 않습니다
rule 안의 clearBoth;Action을 실행합니다. 두 쓰기가 같은 클록 에지에 반영됩니다

noAction은 아무 일도 하지 않는 내장 Action입니다. Action이 필요한 위치에서 상태를 바꾸지 않아야 할 때 사용합니다.

원자적 Action의 규칙은 앞 장의 규칙이며, 이제 모든 method 호출에도 그대로 적용됩니다.

  • Action 안의 레지스터 읽기는 모두 rule이 시작될 때의 값을 봅니다.
  • 쓰기는 모두 클록 에지에서 함께 반영됩니다. 같은 Action 안에서 앞서 쓴 값을 뒤 문장이 읽는 일은 없습니다.
  • Action에서 호출한 method의 준비 조건은 rule의 implicit condition에 더해집니다. method가 준비되지 않으면 명시적 조건과 관계없이 rule은 실행되지 않습니다(9절).

3. rule에서 호출한 method는 그 실행의 일부입니다

rule이 c.bump를 호출하면 bump 안의 쓰기는 rule 자신의 쓰기와 같은 에지에서 반영되고, method 안의 모든 읽기는 사이클 시작 상태를 봅니다. method 본문은 먼저 실행되는 절차가 아닙니다. rule의 전이 하나에 더해지는 회로입니다.

ActionMethod.bsv의 Counter에는 상태를 바꾸는 bump와 상태를 읽는 value가 있습니다. rule drive는 step < 3인 동안 매 사이클 c.value를 출력한 뒤 c.bump를 호출합니다. rule done은 step이 3일 때 최종 값을 출력합니다. drive 안에서 출력문이 c.bump보다 먼저 적혀 있다는 점에 주목하세요. 이 순서가 결과에 영향을 주는지가 이 예제의 질문입니다.

실행하기 전에 예측해 보세요. drive가 세 번 실행되는 동안 출력되는 value는 각각 얼마일까요? done이 출력하는 최종 값은 얼마일까요?

확인 문제

drive의 첫 실행에서 value는 무엇을 출력합니까?

ActionMethod.bsv
package ActionMethod;

// Action method는 상태를 변경하고 아무 값도 반환하지 않는다.
interface Counter;
  method Action bump;
  method Bit#(8) value;
endinterface

module mkCounter(Counter);
  Reg#(Bit#(8)) count <- mkReg(0);

  method Action bump;
    count <= count + 1;
  endmethod

  method Bit#(8) value = count;
endmodule

(* synthesize *)
module mkTop(Empty);
  Counter c <- mkCounter;
  Reg#(Bit#(4)) step <- mkReg(0);

  rule drive (step < 3);
    // value method는 rule이 시작될 때의 상태를 읽는다. 따라서 bump가 반영되기 전의
    // 값을 출력한다.
    $display("step %0d: value = %0d", step, c.value);
    c.bump;
    step <= step + 1;
  endrule

  rule done (step == 3);
    $display("final value = %0d", c.value);
    $finish(0);
  endrule
endmodule

endpackage

이 개념 실습하기

다음 단계로 짧은 BSV 문제에 이 개념을 적용해 보세요.

결과 확인. 사이클마다 상태를 추적하면 다음과 같습니다.

사이클시작할 때 count실행되는 rulevalue가 읽는 값에지 뒤 count
00drive01
11drive12
22drive23
33done33

step 줄 세 개는 0, 1, 2를 보여 줍니다. drive 안의 c.value는 같은 rule의 c.bump가 예약한 쓰기를 보지 못하고 사이클이 시작될 때의 상태를 읽습니다. 마지막 줄의 3은 세 번째 쓰기가 반영된 다음 사이클에 done이 읽은 값입니다.

직접 바꿔 보세요.

확인 문제

drive에서 $display 줄을 c.bump; 아래로 옮기면 무엇이 바뀝니까?

답

이어서 bump의 count + 1을 count + 2로 바꿔 보세요. step 줄은 0, 2, 4를, 마지막 줄은 6을 출력합니다.

4. actionvalue와 <-

ActionValue#(t)는 원자적 Action이면서 타입 t의 값을 반환합니다. actionvalue 블록 안에서는 return으로 결과를 지정합니다.

형식

actionvalue
  문장;
  return 식;
endactionvalue

타입 이름 <- actionvalue_호출;
let 이름 <- actionvalue_호출;
method ActionValue#(UInt#(8)) issue;
  next <= next + 1;
  return next;
endmethod

let ticket <- dispenser.issue;

<-는 오른쪽 action을 실행하고 그 결과 값을 이름에 결합합니다. 모듈 인스턴스화에 쓰는 연산자와 같습니다. 모듈 인스턴스화도 모듈 계산을 실행해 결과 인터페이스를 받는 동작이기 때문입니다. =를 쓰면 실행되지 않은 식 자체가 결합됩니다. 예를 들어 UInt#(8) ticket = dispenser.issue;는 기대 타입 UInt#(8), 추론된 타입 ActionValue#(UInt#(8))이라는 T0020 오류가 됩니다.

5. ActionValue는 진입 값을 반환하고 다음 값을 예약합니다

ActionValueMethod.bsv의 take는 다음 티켓 번호를 예약하면서 현재 번호를 반환합니다. 한 호출에서 next를 반환하고 next + 1을 씁니다. 호출하는 쪽 drive는 결과를 let issued <- t.take;로 받습니다.

실행하기 전에 예측해 보세요. 호출마다 "지금 반환되는 값"과 "다음 사이클에 next에 보이는 값"을 한 쌍으로 적어 보세요.

확인 문제

next는 1에서 시작합니다. 첫 번째로 발급되는 값은 무엇입니까?

ActionValueMethod.bsv
package ActionValueMethod;

// ActionValue method는 상태를 변경하고 값도 반환한다.
interface Ticket;
  method ActionValue#(Bit#(8)) take;
endinterface

module mkTicket(Ticket);
  Reg#(Bit#(8)) next <- mkReg(1);

  method ActionValue#(Bit#(8)) take;
    next <= next + 1;
    return next;
  endmethod
endmodule

(* synthesize *)
module mkTop(Empty);
  Ticket t <- mkTicket;
  Reg#(Bit#(4)) step <- mkReg(0);

  rule drive (step < 3);
    // ActionValue method의 결과는 <-로 받는다. 모듈 인스턴스화와 같은 연산자다.
    let issued <- t.take;
    $display("issued %0d", issued);
    step <= step + 1;
  endrule

  rule done (step == 3);
    $finish(0);
  endrule
endmodule

endpackage

이 개념 실습하기

다음 단계로 짧은 BSV 문제에 이 개념을 적용해 보세요.

결과 확인.

호출반환값다음 사이클의 next
1번째12
2번째23
3번째34

issued 줄 세 개가 1, 2, 3이면 take가 갱신 전 값을 반환한다는 것이 확인됩니다. 반환과 예약이 한 method 호출 안에 있으므로 8비트 카운터가 한 바퀴 돌기 전에는 두 클라이언트가 같은 번호를 받지 않습니다. 호출이 256번 받아들여지면 번호가 다시 반복됩니다.

직접 바꿔 보세요.

바꿀 곳예상 결과
mkReg(1)을 mkReg(5)로issued 줄이 5, 6, 7이 됩니다
let issued <- t.take;를 Bit#(8) issued = t.take;로컴파일 오류 T0020이 발생합니다. 기대한 타입은 Bit#(8)이고 실제 타입은 ActionValue#(Bit#(8))입니다

주의. Bit#(8) issued = t.take;가 타입 오류인 이유는 =가 실행되지 않은 동작을 결합하고, 그 타입이 Bit#(8)이 아니라 ActionValue#(Bit#(8))이기 때문입니다. <-만 동작을 실행합니다.

6. 예제: 현재 번호표 발급

ActionValueTicket.bsv의 발급기는 issue method가 번호를 반환하면서 카운터를 증가시킵니다. 이번에는 actionvalue … endactionvalue 블록을 직접 적었습니다. TicketDispenser 인터페이스, next 레지스터와 issue method, issueTwo와 그 안의 if 순서로 읽으세요.

초기 상태는 next = 40, issued = 0입니다.

실행시작 상태 (next, issued)반환값예약된 다음 상태
첫 번째(40, 0)40(41, 1)
두 번째(41, 1)41(42, 2)와 $finish

두 번째 실행의 if (issued == 1)도 시작 시점의 값을 읽으므로, 두 번째 번호를 출력한 직후 시뮬레이션이 끝납니다.

실행하기 전에 예측해 보세요. return next;는 바로 위 줄의 next <= next + 1;이 예약한 값을 볼까요?

확인 문제

어떤 번호 두 개가 어떤 순서로 출력됩니까?

답

ActionValueTicket.bsv
package ActionValueTicket;

interface TicketDispenser;
  method ActionValue#(UInt#(8)) issue;
endinterface

module mkTicketDispenser(TicketDispenser);
  Reg#(UInt#(8)) next <- mkReg(40);

  method ActionValue#(UInt#(8)) issue;
    actionvalue
      next <= next + 1;
      // 반환값은 예약된 레지스터 갱신이 반영되기 전에 읽힌다.
      return next;
    endactionvalue
  endmethod
endmodule

(* synthesize *)
module mkTop(Empty);
  TicketDispenser dispenser <- mkTicketDispenser;
  Reg#(UInt#(2)) issued <- mkReg(0);

  rule issueTwo (issued < 2);
    let ticket <- dispenser.issue;
    $display("ticket=%0d", ticket);
    issued <= issued + 1;
    if (issued == 1) $finish(0);
  endrule
endmodule

endpackage

이 개념 실습하기

다음 단계로 짧은 BSV 문제에 이 개념을 적용해 보세요.

결과 확인.

출력 줄확인되는 사실
첫 ticket이 40return next가 예약된 41이 아니라 시작 상태를 읽었습니다
두 번째 ticket이 41예약된 쓰기가 두 번째 실행 전의 에지에서 반영되었습니다

반환값과 예약된 다음 상태는 같은 원자적 Action에 속하지만 서로 다른 시점의 값입니다.

직접 바꿔 보세요.

  1. 계약을 "증가한 뒤 그 번호를 보고"로 바꾸어 보세요. 쓰기는 더하지 않고 return next;만 return next + 1;로 바꿉니다. 출력은 41과 42가 되고 레지스터는 여전히 40, 41, 42로 변합니다. 반환 계약만 바뀌었습니다.
  2. return next;를 next <= next + 1; 위로 옮긴다고 생각해 보세요. 결과는 같습니다. Action 안의 문장 순서는 읽기가 보는 값을 바꾸지 않습니다.

7. 모듈 안의 rule

rule은 모듈의 내부 동작을 기술합니다. 이름, 선택적인 명시 조건, action 본문으로 이루어지며, 본문에서 모듈이 가진 하위 인스턴스의 method를 호출할 수 있습니다.

형식

rule 이름 (명시_조건);
  action_문장;
endrule
rule drain (step == 3 && stack.notEmpty);
  let value <- stack.pop;
  $display("%0d", value);
endrule
부분의미
drain모듈 안에서 고유한 rule 이름입니다
(step == 3 && stack.notEmpty)명시 조건입니다. 생략하면 항상 참입니다
stack.pop호출한 method입니다. pop의 implicit condition이 활성화 조건에 더해집니다

rule이 실행되면 본문의 모든 상태 갱신이 하나의 원자적 전이로 반영됩니다. 호출이 여러 하위 모듈의 상태를 바꾸더라도 일부만 반영된 상태는 관찰되지 않습니다. 여러 rule이 같은 상태를 갱신할 때 필요한 중재와 멀티플렉서 회로는 bsc가 생성합니다.

8. 타입 매개변수가 있는 인터페이스와 하위 인터페이스

8.1 타입 매개변수가 있는 인터페이스

인터페이스 이름 뒤에 #(type 이름)을 붙이면 원소 타입을 고정하지 않은 인터페이스를 만들 수 있습니다.

형식

interface 인터페이스_이름#(type 타입_변수);
  ...
endinterface
interface Holder#(type t);
  method Action put(t x);
  method t get;
endinterface

Holder#(Bit#(8)) small;   // 원소가 Bit#(8)인 인터페이스 타입
Holder#(Bool)    flag;    // 원소가 Bool인, 위와 다른 인터페이스 타입

인스턴스마다 매개변수에 구체적인 타입 하나가 정해집니다. Holder#(Bit#(8))과 Holder#(Bool)은 서로 다른 타입입니다.

참고. 매개변수가 크기를 나타내는데 사용 형태만으로 그 사실을 알 수 없으면 numeric type depth처럼 numeric type으로 선언합니다.

8.2 하위 인터페이스와 Empty

인터페이스 안에 다른 인터페이스를 멤버로 둘 수 있습니다. 멤버 앞에 interface를 쓰고, 클라이언트는 점 표기로 접근합니다.

interface Channel;
  interface Holder#(Bit#(8)) data;
  method Action clear;
endinterface

// 사용하는 쪽
ch.data.put(8'h2a);

작은 인터페이스를 조합해 큰 인터페이스를 만들면 같은 계약을 여러 모듈에서 재사용할 수 있습니다. Clock과 Reset도 인터페이스 타입이므로 하위 인터페이스로 둘 수 있습니다.

프렐류드의 Empty는 method가 하나도 없는 인터페이스입니다. 외부에 아무 기능도 제공하지 않는 최상위 모듈은 module mkTop(Empty);처럼 Empty를 제공합니다.

9. method는 거절할 수 있습니다: implicit condition

method 정의의 헤더에 if (조건)을 붙일 수 있습니다. 이 조건이 method의 준비 조건이고, implicit condition이라고도 합니다. 조건이 거짓인 사이클에 method는 준비되지 않고, 그 method를 호출하는 rule은 자기 guard가 무엇이든 전혀 실행되지 않습니다. 호출만 건너뛰고 나머지를 실행하는 일은 없습니다. 실행 전체가 보류됩니다.

형식

method Action push(Bit#(8) x) if (depth < 2);
  stored <= x;
  depth  <= depth + 1;
endmethod

호출하는 rule의 guard와 그 rule이 호출하는 모든 method의 준비 조건은 합쳐집니다. 모두 참일 때만 rule이 준비됩니다. 준비 조건은 불변식이 있는 곳인 method에 둡니다. 그러면 모든 호출자가 검사를 반복하지 않고도 보호됩니다.

주의. method 본문 안의 if는 다른 것입니다. push 안에 if (depth < 2) begin … end를 쓰면 method는 항상 준비되어 있고, 호출하는 rule은 계속 실행되며 쓰기만 건너뜁니다. 준비 조건은 헤더에 있습니다.

앞 장은 rule이 준비된 것과 실행된 것을 구분했습니다. method는 rule이 멈춘 듯 보일 때 확인할 세 번째 사실을 더합니다. 그 rule이 호출하는 모든 method가 준비되어 있는가? 디버깅 순서는 호출한 method의 준비 조건, 그 다음 rule 자신의 guard, 그 다음 다른 rule과의 충돌입니다. 어느 단계에서 실패해도 밖에서는 같아 보입니다. 아무것도 바뀌지 않습니다. 다음 장에서는 자기 guard가 없는 rule이 push의 준비 조건 때문에 멈추는 예제를 실행하고, 준비 조건이 참조할 수 있는 이름의 규칙을 다룹니다.

10. 적용: 호출 한 번에 전이 하나

실습 문제가 네 개입니다. 채점 문제에서 bump는 count <= ?;를 쓰고 카운터는 임의의 값으로 끝납니다. don't-care를 현재 count에서 유도한 다음 값으로 바꿔 호출 한 번이 정확히 한 번의 증가가 되게 하세요. 두 번째 문제의 step(clear, enable)은 두 명령을 Bool 인자로 받는 Action method 하나입니다. clear가 이기고 enable은 세고 둘 다 없으면 유지하도록 본문을 쓰세요. 설계 장이 요구사항에서 이 본문을 이끌어 냅니다. 나머지 두 문제에서 take와 issue는 각각 next + 1을 반환해 발급되는 모든 티켓이 하나씩 앞서 있습니다. 쓰기는 그대로 두고 반환 식만 진입 값으로 바꾸세요.

11. 자주 하는 실수

11.1 ActionValue method의 결과를 =로 받기

// 잘못된 코드: 결과 값이 아니라 action 자체를 받습니다
Bit#(8) issued = t.take;

// 바른 코드
Bit#(8) issued <- t.take;

=는 오른쪽 식을 실행하지 않고 그대로 이름에 결합합니다. 타입을 적었다면 T0020 오류가 기대한 타입 Bit#(8)과 실제 타입 ActionValue#(Bit#(8))을 함께 보여 줍니다. 이 오류는 Action의 기술과 실행 결과를 혼동했다는 신호입니다. let issued = t.take;처럼 타입을 생략하면 issued가 action 자체가 되어 오류가 늦게 나타납니다. 이 값을 issued + 1처럼 계산에 쓰면 Arith#(ActionValue#(Bit#(8))) 인스턴스가 없다는 T0031 오류가 발생합니다.

11.2 현재 값 대신 예약된 값을 반환하기

// "발급한 뒤 증가" 계약에서는 잘못된 코드: 다음 번호를 반환합니다
next <= next + 1;
return next + 1;

// 바른 코드: 시작 상태를 반환합니다
next <= next + 1;
return next;

두 코드 모두 컴파일됩니다. 반환값이 늘 한 단계 앞선다면 return 식과 레지스터의 시작 상태를 비교해 보세요.

11.3 pop에서 상태 일부만 갱신하기

// 잘못된 코드: depth만 줄입니다
method ActionValue#(t) pop if (depth > 0);
  depth <= depth - 1;
  return items[0];
endmethod

// 바른 코드: 저장된 원소도 함께 옮깁니다
method ActionValue#(t) pop if (depth > 0);
  items <= shiftInAtN(items, ?);
  depth <= depth - 1;
  return items[0];
endmethod

잘못된 코드도 타입 검사를 통과합니다. 같은 원소가 계속 반환되는 증상이 보이면 method가 갱신해야 할 상태를 모두 갱신하는지 확인하세요. 이 pop이 속한 스택은 다음 장의 종합 예제입니다.

12. 정리

요소뜻
value method읽기. 식 안에서 호출. 상태를 바꾸지 않음
Action method상태 변경. rule 안에서 문장으로 호출
ActionValue#(t) method상태를 바꾸고 t를 반환. <-로 호출
action … endaction원자적 작업. =로 결합해도 실행되지 않음. noAction은 아무 일도 하지 않음
actionvalue … endactionvalue원자적 작업과 시작 상태로 계산한 결과. <-로 실행
<-모듈 계산이나 action을 실행하고 결과를 결합. =는 식을 실행하지 않고 결합
호출 안읽기는 진입 상태를 봄. 쓰기는 rule의 쓰기와 함께 반영
method 헤더의 if (조건)준비 조건. 거짓이면 호출한 rule이 실행되지 않음
준비와 실행method 준비 조건, rule의 guard, 충돌은 세 가지 별개의 검사
인터페이스#(type t), 하위 인터페이스, Empty사용할 때마다 정해지는 원소 타입. 점 표기로 접근하는 중첩 인터페이스. method가 없는 인터페이스

13. 확인 문제

  1. 큐에서 항목을 제거하면서 그 항목을 반환해야 합니다. 어떤 method 종류가 알맞을까요?

    정답 보기

    ActionValue method입니다. 반환과 제거가 한 호출 안에 있어야 제거한 값과 반환한 값이 언제나 같습니다.

  2. 한 rule 안에서 c.bump; 다음 줄에 c.value를 읽으면 갱신된 값을 볼 수 있을까요?

    정답 보기

    볼 수 없습니다. 한 rule의 모든 호출은 레지스터를 사이클 시작 시점의 값으로 읽습니다. bump가 예약한 값은 다음 사이클에 보입니다.

  3. Action clearBoth = action ... endaction;이 어떤 레지스터도 지우지 않는 이유는 무엇일까요?

    정답 보기

    =는 작업의 기술에 이름을 붙일 뿐입니다. 쓰기는 rule 본문 같은 Action 문맥에서 clearBoth를 문장으로 실행할 때 일어납니다.

  4. ActionValue method가 next <= next + 1을 쓰고 next를 반환합니다. 호출한 쪽은 어떤 값을 받을까요?

    정답 보기

    실행이 시작될 때의 값을 받습니다. 쓰기는 클록 에지에서 반영되므로 증가한 값은 다음 실행에서야 보입니다.

다음 장은 method의 준비 조건을 호출자까지 따라가고, 모듈 정의에 매개변수와 인자를 더하며, 합성 경계에서 인터페이스가 지켜야 할 조건을 정합니다.

확인 지점

실습 문제를 제출해 채점을 받습니다

채점은 서버의 비공개 테스트로 진행되며, 결과는 이 브라우저에만 기록됩니다.

참고 자료

참고한 자료의 라이선스와 고지