발행일

동의를 어디서 받을지 정하려고 화면부터 만들었다 — 가입 약관 단계와 개인정보처리방침을 테스트로 묶기

동의를 어디서 받을지 정하려고 화면부터 만들었다

먼저 밝혀 둡니다. 이 글의 세 커밋(e37f4a67·7cf4e61d·ad759177, 9월 28일)은 통합로그인 미리보기용 브랜치에만 있고, 10월 말까지 다른 작업 브랜치에 병합되지 않았습니다. 운영에 나간 기능이 아니라, 정해지지 않은 결정을 화면으로 먼저 보여 주려고 만든 목업과 그 일부를 실제 가입에 붙여 본 기록입니다. 이 글은 그 경과까지 묶어 10월 말에 정리했습니다.

연계 흐름에 동의 단계가 없었다

이 시스템은 다른 시스템 하나와 계정을 잇는 통합로그인을 준비하고 있었습니다. 회원 연계 모듈은 팀원이 만들었고, 가입·정보 수정·비밀번호·로그인·로그아웃을 양쪽으로 맞추는 데까지 테스트베드에 올라가 있었어요. 외부 기관이 운영하는 다른 통합로그인도 하나 더 붙일 예정이었습니다.

그런데 연계 문서 어디에도 약관 동의 단계가 없었습니다. 로그인 흐름은 시작부터 완료까지 곧장 이어지고, 공개 가입이 곧 양쪽 가입이라 회원이 통합로그인 여부를 따로 고를 자리도 없었어요. 둘러보니 그 앞의 문제도 있었습니다.

  • 지금 회원가입 화면에는 개인정보 수집 동의가 아예 없었습니다.
  • 개인정보처리방침 제4조는 "제3자에게 제공하고 있지 않습니다"라고 적고 있었는데, 연계를 켜면 가입 정보가 다른 시스템으로 넘어갑니다. 설정 하나로 방침과 실제 동작이 어긋나게 되는 구조였어요.

결정 대신 위치별 목업

동의를 어디서 받을지는 제가 정할 수 있는 일이 아니었습니다. 가입 때 받으면 신규 회원만 해결되고, 기존 회원은 다른 시스템으로 넘어가기 직전에 따로 받아야 합니다. 다만 운영은 데이터를 초기화하고 신규 가입으로 시작할 예정이라, 기존 회원 쪽이 실제로 필요한 경우는 약관이 바뀌어 다시 동의를 받을 때 정도였어요. 어느 쪽이냐에 따라 백엔드가 로그인 흐름에 단계를 얼마나 끼워야 하는지가 갈려요. 약관 문구는 법적 고지라 기관 검토도 거쳐야 하고요.

그래서 e37f4a67(+723줄)에서는 결정 대신 동의 위치마다 화면을 하나씩 만들었습니다.

안화면
① 신규 가입약관 동의 → 정보 입력 → 가입 접수, 세 단계(셋째가 '완료'가 아니라 '접수'인 건 팀원이 만든 가입 승인 흐름에서 메일 확인 전에는 로그인이 막혀 있어서)
② 기존 회원다른 시스템으로 이동하기 직전 한 번
마이페이지외부 통합로그인 도입 자료가 개발 항목으로 든 가입 여부 표시, 등록·연결 해제 확인 팝업

목업 주소는 전부 if settings.DEBUG: 블록 안에 두고, 저장이 일어나지 않게 했습니다. 정보 입력 단계는 실제 가입 폼을 그대로 쓰되 [회원가입]을 눌러도 저장 없이 다음 단계로만 넘기고, 마이페이지 목업은 GET만 열어 폼을 제출하면 405가 납니다. 실제 화면 코드는 공유하고 실제 데이터는 건드리지 않았습니다.

동의 화면 두 개는 목업용 DEBUG 블록 밖의 정식 주소로 두었지만, 로그인 흐름에는 연결하지 않은 독립 화면입니다. 외부 통합로그인 쪽은 받은 화면 틀에 상대가 준 참고 자료로 약관을 채웠어요. 필수 약관 셋이 빠지면 JS와 서버 양쪽에서 막고, 동의 뒤 ?next=로 돌려보낼 때는 url_has_allowed_host_and_scheme으로 다른 사이트 주소면 홈으로 보냅니다. 돌아갈 주소를 요청에서 받는 자리라 열린 리다이렉트가 되지 않게 한 거예요.

목업 문서에는 정해야 할 것을 따로 적었습니다. 안 ①만 쓸지 ①과 ②를 함께 쓸지, 동의 기록을 어디에 저장할지, 그리고 연계 상대가 같은 기관이 운영하는 시스템이라면 "제3자 제공" 동의 자체가 필요 없을 수도 있다는 점이에요. 마지막 것은 법적 판단이라 질문으로만 남겼습니다.

가입 폼 맨 아래에 체크박스를 붙이는 안도 목업으로 만들어 봤습니다. 가입 폼이 이미 길다는 불만이 있던 터라 더 늘리지 않기로 했습니다. 그 안은 코드를 남기지 않고 지워서 커밋에는 없습니다.

안 ①의 동의 단계만 실제 가입에 붙였다

같은 날 저녁 7cf4e61d에서 안 ①의 동의 단계를 실제 /user/signup에 붙였습니다. 가입 화면에 수집 항목을 알리고 동의를 받는 단계가 아예 없는 건 연계와 상관없는 빈틈이라고 봤고, 그래서 연계 결정을 기다리지 않고 붙일 수 있는 부분이었어요.

필수 동의는 이용약관과 개인정보 수집·이용 둘이고, 연계가 켜지면 제3자 제공이 셋째로 붙습니다. 동의 여부는 세션에 두고, 폼 제출도 같은 조건으로 막습니다.

def post(self, request, *args, **kwargs):
    if request.POST.get("consent_step"):
        if not all(request.POST.get(name) for name in signup_consent_fields()):
            return self.render_consent(consent_error=True, checked=request.POST)
        request.session[SIGNUP_CONSENT_KEY] = True
        return redirect("common:user_signup")
    if not request.session.get(SIGNUP_CONSENT_KEY):
        return redirect("common:user_signup")
    return super().post(request, *args, **kwargs)

화면에서만 다음 단계를 막으면 가입 POST를 바로 보내 동의를 건너뛸 수 있어서, 서버가 동의 없는 제출을 1단계로 돌려보내게 했습니다. 가입이 끝나면 세션의 동의 표시를 지우는데, 연계 가입은 진행 화면으로 넘어가는 리다이렉트가 나올 때만 끝난 것으로 봅니다. 중간에 오류로 폼이 다시 그려지는 경우에는 동의를 다시 받지 않게요.

이 관문 때문에 팀원이 만든 연계 테스트 일부가 깨졌습니다. 가입 POST를 바로 보내던 테스트였어요. 테스트가 확인하려는 것(중복 이메일 오류, 아이디 길이 하한 등)은 그대로 두고, 입력 경계 테스트에는 같은 주소로 동의부터 보내는 post_signup 헬퍼를 두었고, 가입 승인 테스트 한 곳은 세션에 동의 표시를 직접 넣었습니다.

방침의 두 조항을 실제 동작에 맞췄다

제4조는 연계 설정을 따라 바뀌게 했습니다. 꺼져 있으면 "제공하고 있지 않습니다"를 그대로 두고, 켜지면 그 자리에 제공받는 자·이용 목적·제공 항목·보유 기간 표가 나옵니다. 표의 값은 가입 1단계의 제3자 제공 칸과 같은 목록입니다.

제3조는 같은 날 ad759177에서 고쳤습니다. 동의 화면의 수집 항목을 가입 폼 기준으로 적다 보니, 방침 제3조의 "회원가입 및 관리" 줄이 폼과 달랐어요.

방침(전)가입 폼이 실제로 받는 것
필수성명, 아이디, 비밀번호, 이메일, 전화번호, 소속기관아이디, 비밀번호, 이메일, 이름
선택부서명휴대전화번호, 주소

가입 폼은 소속기관과 부서명을 받지 않습니다. 소속기관은 가입 뒤 관리자가 목록에서 지정하는 값이라 "서비스 제공" 줄에는 그대로 두었어요. 방침이 받지도 않는 항목을 수집한다고 적고, 실제로 받는 주소는 빠뜨린 상태였습니다.

이 목록은 세 곳에 있습니다. 가입 폼의 칸, 방침 제3조, 가입 동의 화면의 수집 항목이에요. 하나만 바뀌어도 다시 어긋나니, 셋이 어긋나면 실패하는 테스트를 붙였습니다.

def test_방침_제3조_가입_항목은_가입_폼과_같다(self):
    from user_org.forms import UserSignupForm

    # 폼 칸이 늘거나 줄면 방침 제3조와 가입 동의 화면의 수집 항목도 같이 고쳐야 한다
    self.assertEqual(
        UserSignupForm._meta.fields,
        (
            "username",
            "password1",
            "password2",
            "email",
            "name",
            "cellphone",
            "address",
        ),
    )
    policy = self.client.get(reverse("common:legal_privacy_policy"))
    self.assertContains(policy, "아이디, 비밀번호, 이메일, 이름")
    self.assertContains(policy, "휴대전화번호, 주소")
    self.assertContains(
        self.client.get(self.url()),
        "(필수) 아이디, 비밀번호, 이메일, 이름 / (선택) 휴대전화번호, 주소",
    )

폼의 칸 목록을 상수로 단언하는 건 거친 방법입니다. 칸 순서만 바뀌어도 실패하니까요. 지금 돌아보면 폼에서 목록을 뽑아 방침을 자동으로 그리는 길도 있었지만, 방침 문구는 코드가 만들어 낼 문장이 아니라서 실패를 보고 사람이 다시 읽고 고치는 지금 방식이 낫다고 봅니다. 테스트의 주석도 "같이 고쳐야 한다"고만 적었습니다.

가입 동의 단계에는 테스트가 6건 더 있습니다. 동의 전에는 약관 화면이 나오는지, 동의 없이 제출하면 돌려보내는지, 필수 동의가 빠지면 오류와 함께 남는지, 동의 뒤 폼이 열리고 가입 뒤 동의가 지워지는지, 연계가 켜지면 제3자 제공까지 받는지, 제4조가 설정을 따르는지예요.

그 뒤

미리보기 브랜치는 9월 28일 이 세 커밋에서 멈춰 있습니다. 10월 말 기준으로 다른 작업 브랜치의 방침 제3조는 여전히 옛 항목(성명·전화번호·소속기관·부서명)이고, 제4조는 연계 설정과 상관없이 "제공 없음"입니다. 가입에도 동의 단계가 없어요. 연계 모듈 쪽 문서도 그 뒤 정리됐지만 동의 단계는 들어가지 않았습니다.

목업이 묻던 질문들, 곧 동의를 어디서 받을지와 제3자 제공으로 볼지는 10월 말까지 저장소 어디에도 답이 기록되지 않았고, 미리보기 브랜치도 원격에 올리지 않은 채였습니다. 다만 제3조 정정은 연계와 무관하게 지금 방침이 틀린 부분이라, 이것만 따로 떼어 내지 않은 건 아쉬운 점입니다.

한계

  • 병합되지 않았습니다. 위에 적은 대로, 이 글의 어떤 화면도 운영에 나가지 않았습니다. 현행 방침 제3조가 가입 폼과 어긋나 있는 것도 그대로예요.
  • 동의 기록을 저장하지 않습니다. 실제 가입에 동의를 받기 시작했지만, 언제 어느 판의 약관에 동의했는지 남길 자리는 모델과 마이그레이션이 필요한 일이라 문서에 과제로만 남겼습니다.
  • 약관 문구는 모두 초안입니다. 연계 모듈이 실제로 넘기는 항목과 기존 방침의 목적·보유 기간을 근거로 채웠지만, 기관 검토를 거치지 않았습니다. 외부 통합로그인 쪽은 운영 기관명과 그쪽 보유 기간을 받은 자료에서 확인하지 못해 "확정 필요"로 비워 뒀습니다.
  • 다운로드용 방침 파일은 그대로입니다. [개인정보처리방침 다운로드]는 고정 텍스트 파일이라 연계를 켜도 제4조가 바뀌지 않고, 제3조도 옛 판입니다. 법적 고지라 기관 검토와 함께 고치기로 하고 손대지 않았습니다.